#!/usr/bin/env python3.3
# -*- coding: utf-8 -*-
import os
import sys
import socket
import ssl
import logging.handlers
import json
import multiprocessing.connection
import subprocess
import threading
import queue
import collections
import time
import types
import copy
import pprint

VERSION = '0.1'
SOURCE_URL = 'https://code.google.com/p/noisebot/source/browse/'

try:
	user_data_dir = os.path.join(os.environ['APPDATA'], 'NoiseBot')
except KeyError:
	user_data_dir = os.path.join(os.environ['HOME'], '.NoiseBot')

base_dir = os.getcwd()
global_data_dir = os.path.join(base_dir, 'data')

class JSONEncoderWithIter(json.JSONEncoder):
	def default(self, o):
		try:
			iterable = iter(o)
		except TypeError:
			pass
		else:
			return list(iterable)
		return json.JSONEncoder.default(self, o)
json_decoder = json.JSONDecoder(strict=False)
json_encoder = JSONEncoderWithIter(separators=(',', ':'))

with open(os.path.join(global_data_dir, 'default_global.json')) as default_global:
	try:
		config = json_decoder.decode(default_global.read())
	except ValueError:
		print('Default config file could not be decoded. This file should never be modified - use global.json instead!')
		raise
with open(os.path.join(global_data_dir, 'global.json')) as global_conf_file:
	try:
		global_conf = json_decoder.decode(global_conf_file.read())
	except ValueError:
		print('global.json is not valid JSON. For more help, paste the config into a lint tool such as the one at http://jsonlint.com/ .')
		raise
	for x in global_conf.keys():
		config[x].update(global_conf[x])

try:
	network_name = sys.argv[1]
except IndexError:
	network_name = config['config']['default_network']
if config['config']['single_user_mode']:
	data_dir = global_data_dir
else:
	data_dir = user_data_dir

# Create data directories
for x in (data_dir, os.path.join(data_dir, 'networks'), os.path.join(data_dir, 'networks', network_name, 'logs')):
	try:
		os.mkdir(x)
	except FileExistsError:
		pass

network_file = os.path.join(data_dir, 'networks', network_name, 'config.json')
with open(network_file) as network_conf_file:
	try:
		network_conf = json_decoder.decode(network_conf_file.read())
	except ValueError:
		print('The config file for {} is not valid JSON. For more help, paste the config into a lint tool such as the one at jslint.com.'.format(network_name))
		raise
	for x in network_conf.keys():
		config[x].update(network_conf[x])

log_str_to_level = {'debug':logging.DEBUG,
					'info':logging.INFO,
					'warning':logging.WARNING,
					'error':logging.ERROR,
					'critical':logging.CRITICAL,
					'fatal':logging.FATAL}
msg_log_str_to_level = log_str_to_level.copy()
# extra levels for the message logger
CHAT = 21 # standard messages
CHANNEL = 22 # part/join/mode/topic changes
NOTICE = 25 # important information that can affect the operation of the bot (e.g., notices, user mode changes)
msg_log_str_to_level.update(chat=CHAT, channel=CHANNEL, notice=NOTICE)

def is_in(value, iterable):
	try:
		if value in iterable:
			return True
		else:
			return False
	except:
		return False

other_key = None
valid_config_values = {'identity':{'preferred_nicks': (isinstance, type(list()), 'must be a list'),
									'user': (isinstance, type(str()), 'must be a string'),
									'name': (isinstance, type(str()), 'must be a string'),
									'identify_to_services': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'services_pass': (isinstance, type(str()), 'must be a string'),
									'trigger': (isinstance, type(str()), 'must be a string')},
					'users':{'use_cloak_auth': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'use_nick_auth': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'owner_cloaks': (isinstance, type(list()), 'must be a list'),
							'owner_nicks': (isinstance, type(list()), 'must be a list'),
							'owners_who_recv_notifs': (isinstance, type(list()), 'must be a list'),
							'owner_notification_level': (is_in, ('info', 'warning', 'error', 'critical', 'off'), 'must be a valid level'),
							'use_memo_service': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'ignored_nicks': (isinstance, type(list()), 'must be a list'),
							'ignored_cloaks': (isinstance, type(list()), 'must be a list')
							},
					'network':{'name': (isinstance, type(str()), 'must be a string'),
							'address': (isinstance, type(str()), 'must be a string'),
							'prefer_ssl': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'ssl_only': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'non_ssl_port': (is_in, range(1, 65535), 'must be an integer between 1 and 65535 (i.e., a valid port number)'),
							'ssl_port': (is_in, range(1, 65535), 'must be a number between 1 and 65535 (i.e., a valid port number)'),
							'ssl_protocols': (isinstance, type(list()), 'must be a list'),
							'ping_timeout': (isinstance, (type(float()), type(int())), 'must be a number'),
							'detect_timeout_value': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'nick_service': (isinstance, type(str()), 'must be a string'),
							'channel_service': (isinstance, type(str()), 'must be a string'),
							'memo_service': (isinstance, type(str()), 'must be a string')
							},
					'channels':{'accept_invites': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'join_when_invited_by_default': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'autojoin_channels': (isinstance, type(str()), 'must be a string'),
							'invite_whitelist': (isinstance, type(list()), 'must be a list'),
							'invite_blacklist': (isinstance, type(list()), 'must be a list'),
							'rejoin_if_kicked': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'knock_if_not_invited': (isinstance, type(bool()), 'must be true or false (without quotes)')
							},
					'modules':{'modules': (isinstance, type(list()), 'must be a list'),
							'module_connection_port': (is_in, range(1, 65535), 'must be an integer between 1 and 65535 (i.e., a valid port number)'),
							'module_connection_timeout': (isinstance, (type(float()), type(int())), 'must be a number'),
							'exit_if_module_fails_to_load_at_startup': (isinstance, type(bool()), 'must be true or false (without quotes)'),
							'exit_if_module_fails_at_runtime': (isinstance, type(bool()), 'must be true or false (without quotes)')
							},
					'event_logging':{'console_verbosity': (is_in, log_str_to_level.keys(), 'must be "debug", "info", "warning", "error", or "critical"'),
									'file_verbosity': (is_in, log_str_to_level.keys(), 'must be "debug", "info", "warning", "error", or "critical"'),
									'log_to_console': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'log_to_file': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'rotate_logs': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'write_mode': (isinstance, type(str()), 'must be a string'),
									'encoding': (isinstance, (type(None), type(str())), 'null (without quotes) or a string'),
									'delay_file_open': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'time_formatting': (isinstance, (type(None), type(str())), 'null (without quotes) or a string'),
									'formatting': (isinstance, type(str()), 'must be a string'),
									'formatting_style': (is_in, '%{$', 'must be "%", "{" or "$"')
									},
					'event_log_rotation':{'timed': (isinstance, type(bool()), 'must be true or false (without quotes)'),
										'max_logs': (isinstance, type(int()), 'must be a number'),
										'max_bytes': (isinstance, type(int()), 'must be a number'),
										'rollover_interval': (isinstance, type(int()), 'must be a number'),
										'rollover_period': (is_in, ('H', 'h', 'S', 's', 'D', 'd', 'M', 'm', 'W', 'w', 'midnight'), 'must be "H", "S", "D", "M", "W", or "midnight"'),
										'use_utc': (isinstance, type(bool()), 'must be true or false (without quotes)')
										},
					'message_logging':{'console_verbosity':  (is_in, msg_log_str_to_level.keys(), 'must be "debug", "chat", "info", "notice", "warning", "error", or "critical"'),
									'file_verbosity': (is_in, msg_log_str_to_level.keys(), 'must be "debug", "chat", "info", "notice", "warning", "error", or "critical"'),
									'log_to_console': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'log_to_file': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'rotate_logs': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'encoding': (isinstance, type(str()), 'must be a string'),
									'write_mode': (isinstance, type(str()), 'must be a string'),
									'delay_file_open': (isinstance, type(bool()), 'must be true or false (without quotes)'),
									'default_formatting': (isinstance, type(str()), 'must be a string'),
									'privmsg_formatting': (isinstance, type(str()), 'must be a string'),
									'notice_formatting': (isinstance, type(str()), 'must be a string'),
									'time_formatting': (isinstance, (type(None), type(str())), 'null (without quotes) or a string'),
									'topic_time_format': (isinstance, type(str()), 'must be a string'),
									'creation_time_format': (isinstance, type(str()), 'must be a string'),
									'formatting': (isinstance, type(str()), 'must be a string'),
									'formatting_style': (is_in, '%{$', 'must be "%", "{" or "$"')
									},
					'message_log_rotation':{'timed': (isinstance, type(bool()), 'must be true or false (without quotes)'),
										'max_logs': (isinstance, type(int()), 'must be a number'),
										'max_bytes': (isinstance, type(int()), 'must be a number'),
										'rollover_interval': (isinstance, type(int()), 'must be a number'),
										'rollover_period': (is_in, ('H', 'h', 'S', 's', 'D', 'd', 'M', 'm', 'W', 'w', 'midnight'), 'must be "H", "S", "D", "M", "W", or "midnight"'),
										'use_utc': (isinstance, type(bool()), 'must be true or false (without quotes)')
										}
					}

del config['config']
for section in config.keys():
	config_keys = set(config[section].keys())
	valid_config_keys = set(valid_config_values[section].keys())
	if not valid_config_keys.issubset(config_keys):
		print('Missing keys from {0} section: {1}'.format(section,  ', '.join(valid_config_keys.difference(config_keys))))
		raise ValueError('Missing config values.')
	if config_keys < valid_config_keys:
		print('Extra keys found in the {0} section (these will be ignored): {1}'.format(section, ', '.join(config_keys.difference(valid_config_keys))))
	for key in valid_config_keys:
		other_key = config
		try:
			func, arg, error_msg = valid_config_values[section][key]
		except:
			print(section)
			print(key)
			raise
		config_key = config[section][key]
		if func(config_key, arg):
			continue
		else:
			if config_key is None:
				input_value = 'null'
			elif config_key is True:
				input_value = 'true'
			elif config_key is False:
				input_value = 'false'
			else:
				input_value = config_key
			raise ValueError('{0} is not a valid value for {1} in the {2} section - it {3}'.format(input_value, key, section, error_msg))

id_conf = config['identity']
users_conf = config['users']
net_conf = config['network']
chan_conf = config['channels']
mod_conf = config['modules']
msg_log_conf = config['message_logging']
msg_log_rotation_conf = config['message_log_rotation']
event_logs = types.SimpleNamespace(**config['event_logging'])
event_log_rotation = types.SimpleNamespace(**config['event_log_rotation'])

event_logger = logging.getLogger('noisebot_events')
event_console_handler = logging.StreamHandler()
event_log_formatter = logging.Formatter(fmt=event_logs.formatting, datefmt=event_logs.time_formatting, style=event_logs.formatting_style)
event_console_handler.setFormatter(event_log_formatter)
if event_logs.rotate_logs:
	if event_log_rotation.timed:
		event_file_handler = logging.handlers.TimedRotatingFileHandler(os.path.join(data_dir, 'networks', network_name, 'logs', 'events.log'),
																	when=event_log_rotation.rollover_period,
																	interval=event_log_rotation.rollover_interval,
																	backupCount=event_log_rotation.max_logs,
																	encoding=event_logs.encoding,
																	delay=event_logs.delay_file_open,
																	utc=event_log_rotation.use_utc)
	else:
		event_file_handler = logging.handlers.RotatingFileHandler(os.path.join(data_dir, network_name, 'logs', 'events.log'),
																	mode=event_logs.write_mode,
																	maxBytes=event_log_rotation.max_bytes,
																	backupCount=event_log_rotation.max_logs,
																	encoding=event_log_rotation.encoding,
																	delay=event_log_rotation.delay_file_open)
else:
	event_file_handler = logging.FileHandler(os.path.join(data_dir, network_name, 'logs', 'events.log'),
											mode=event_logs.write_mode,
											encoding=event_logs.encoding,
											delay=event_logs.delay_file_open)

event_log_formatter = logging.Formatter(event_logs.formatting, style=event_logs.formatting_style)
event_console_handler.setFormatter(event_log_formatter)
event_file_handler.setFormatter(event_log_formatter)
event_console_handler.setLevel(log_str_to_level[event_logs.console_verbosity])
event_file_handler.setLevel(log_str_to_level[event_logs.file_verbosity])
if event_logs.log_to_console:
	event_logger.addHandler(event_console_handler)
if event_logs.log_to_file:
	event_logger.addHandler(event_file_handler)
event_logger.setLevel(logging.DEBUG)
event_logger.info('Event logger initialized.')

module_base_dir = os.path.join(base_dir, 'modules')

class Module():
	"""
	name - a string containing the module's name
	config - a dictionary containing the information required to spawn the module
	process - a subprocess.Popen object of the spawned module process
	"""
	def __init__(self, name):
		self.name = name
		self.commands = set()
		self.connection = None
		self.connection_thread = threading.Thread(target=self.recv_message, name='Module Connection Thread ({})'.format(self.name), daemon=True)
		self.is_available = False
		self.incoming_json = collections.deque()
		self.temp_connection_recv_queue = queue.Queue(maxsize=1)
		self.is_dying = False

	def queue_temp_recv_message(self):
		try:
			self.temp_connection_recv_queue.put(self.connection.recv())
		except ConnectionAbortedError:
			if self.is_dying:
				pass

	def recv_message(self):
		while True:
			time.sleep(0.15)
			try:
				self.incoming_json.append(self.connection.recv())
			except (EOFError, ConnectionError, OSError):
				if not self.is_dying:
					event_logger.exception('Connection to {} module seems to be broken:'.format(self.name))
					self.is_available = False
					break
			else:
				event_logger.debug('Received data in recv_message from {}'.format(self.name))

	def load_config(self):
		with open(os.path.join(base_dir, 'modules', '{}.json'.format(self.name))) as config_file:
			self.config = types.SimpleNamespace(**json_decoder.decode(config_file.read()))

	def spawn(self):
		try:
			self.process = subprocess.Popen(self.config.command, **self.config.popen_args)
		except AttributeError:
			self.process = subprocess.Popen(self.config.command)

	def restart(self):
		self.die()
		self.spawn()
	
	def die(self):
		self.is_dying = True
		try:
			self.process.terminate()
		except OSError:
			pass
		if self.connection is not None:
			try:
				self.connection.close()
			except OSError:
				event_logger.exception('Tried to close {} module connection at module death, but failed:'.format(self.name))

class RegistrationCheckJob():
	def __init__(self, network, nick):
		self.network = network
		self.nick = nick
		self.is_pending = True
		self.query_time = time.time()
	
	def set_status_code(self, status_code):
		self._status_code = status_code
		event_logger.debug('Status code for {0} is {1}'.format(self.nick, self._status_code))
		try:
			self.status_code = int(self._status_code)
		except TypeError:
			event_logger.debug('Bad status code! ({})'.format(self.nick))
			self.nick_is_registered = False
		else:
			if self.status_code in (2, 3):
				self.nick_is_registered = True
			elif self.status_code in (0, 1):
				self.nick_is_registered = False
			else:
				event_logger.debug('Bad status code! ({})'.format(self.nick))
				self.nick_is_registered = False
		

class ConnectedServer():
	"""
	A class that represents the server to which the bot is connected. The main purpose of this class is to process and store information from server messages. 
	"""
	def __init__(self, config_network_name):
		self.config_network_name = config_network_name
		self.supported = {}
		self.chanmodes = {}
		self.chanmodetypes = {}
		self.modes_higher_than_op = set()
		self.time_of_last_message = None
		self.time_of_last_ping = None
		self.got_pong = False
		self.prefix_mode_names = {}

	def get_info_from_RPL_MYINFO(self, message_args):
		"""
		<Server address> <Server IRCd> <Supported user modes> <All supported channel modes> <Supported channel modes that take arguments>
		RPL_ISUPPORT provides some of this information and get_info_from_RPL_ISUPPORT overwrites anything set here.
		"""
		self.server_address, self.server_ircd, self.server_user_modes, self.server_chan_modes_all, self.server_chan_modes_args = message_args
		#self.server_chan_modes_all = self.server_chan_modes_no_args + self.server_chan_modes_args
	
	def get_info_from_RPL_ISUPPORT(self, message_args):
		"""
		self.supported stores all parameters and values; certain values are parsed further with more information made available in other variables.
		"""
		for item in message_args:
			if '=' in item:
				param, value = item.split('=')
				self.supported[param.lower()] = value
				if param == 'CHANMODES':
					self.chanmodetypes['list'], self.chanmodetypes['string'], self.chanmodetypes['string_only_when_set'], self.chanmodetypes['boolean'] = value.split(',')
					for mode in self.chanmodetypes['list']:
						self.chanmodes[mode] = set()
					for mode in self.chanmodetypes['string']:
						self.chanmodes[mode] = False
					for mode in self.chanmodetypes['string_only_when_set']:
						self.chanmodes[mode] = False
					for mode in self.chanmodetypes['boolean']:
						self.chanmodes[mode] = False
				elif param == 'CHANTYPES':
					self.chantypes = value
				elif param == 'PREFIX':
					value = value[1:]
					modes, prefixes = value.split(')')
					self.supported['prefix'] = {}
					self.supported['prefix_by_mode'] = {}
					mode_is_higher_than_op = True
					for x in range(len(modes)):
						if modes[x] == 'o':
							mode_is_higher_than_op = False
						if mode_is_higher_than_op:
							self.modes_higher_than_op.add(modes[x])
						self.supported['prefix'][prefixes[x]] = modes[x]
						self.supported['prefix_by_mode'][modes[x]] = prefixes[x]
						self.chanmodes[modes[x]] = set()
					for prefix_mode in self.supported['prefix_by_mode'].keys():
						if prefix_mode == 'q':
							self.prefix_mode_names['q'] = 'owner'
						elif prefix_mode == 'a':
							self.prefix_mode_names['a'] = 'admin'
						elif prefix_mode == 'o':
							self.prefix_mode_names['o'] = 'op'
						elif prefix_mode == 'h':
							self.prefix_mode_names['h'] = 'halfop'
						elif prefix_mode == 'v':
							self.prefix_mode_names['v'] = 'voice'
			else:
				self.supported[item.lower()] = True
		try:
			self.name = self.supported['network']
		except KeyError:
			pass
		event_logger.debug('Processed an RPL_ISUPPORT message.')

	def export_state(self):
		state = copy.deepcopy(self.__dict__)
		return state

class Channel():
	"""
	name (string) - name of the channel, including the prefix.
	names (set) - the nicks currently in the channel, including prefixes.
	nicks (set) - the nicks currently in the channel with prefixes stripped.
	creation_time (int) - the time the channel was created as an int representing the number of seconds since epoch.
	creation_struct_time (time.struct_time) - the time the channel was created as a time.struct_time object.
	modes (dict) - keys are modes; modes that take arguments (bans/excepts/invite excepts/ops) have appropriate values (or False if not set) and modes that don't have either True or False as a value.
	Note that channel modes have different meanings on different IRCds - the bot will not expect any mode to have any particular meaning unless absolutely necessary.
	This class only stores modes and does not apply any meaning to them.
	"""
	def __init__(self, server, name):
		self.name = name
		self.server = server
		self.modes = {}
		self.modes.update(copy.deepcopy(server.chanmodes))
		self.topic = ''
		self.temp_names = set()

	def __repr__(self):
		event_logger.warning('Used channel __repr__ instead of name attribute ({}).'.format(self.name))
		return self.name

	def add_names(self, names):
		"""
		Populates a temporary names list in preparation for an update of names/nicks/modes (typically from 353 RPL_NAMREPLY messages).
		It does NOT request the information from the server.
		"""
		event_logger.debug('number of temp names for {0} before: {1}'.format(self.name, len(self.temp_names)))
		self.temp_names.update(names)
		event_logger.debug('number of temp names for {0} after: {1}'.format(self.name, len(self.temp_names)))

	def update_names(self):
		"""
		Pushes the changes to self.temp_names to self.names and self.nicks (typically after receiving 366 RPL_ENDOFNAMES).
		"""
		nicks = set()
		self.names = self.temp_names
		self.temp_names = set()
		event_logger.debug('Number of names for {0} at update: {1}'.format(self.name, len(self.names)))
		for name in self.names:
			if name[0] in self.server.supported['prefix'].keys():
				nicks.add(name[1:])
			else:
				nicks.add(name)
		self.nicks = nicks
		self.set_modes_from_names()

	def set_modes_from_names(self):
		for mode in self.server.supported['prefix'].values():
			self.modes[mode] = set()
		for name in self.names:
			if name[0] in self.server.supported['prefix'].keys():
				prefix = name[0]
				mode = self.server.supported['prefix'][prefix]
				self.modes[mode].add(name[1:])


	def rebuild_mode_list(self, mode, value):
		"""
		This method rebuilds the list of protected nicks (e.g., from RPL_ALIST/RPL_ENDOFALIST messages); update_modes() appends/removes entries (e.g., from MODE messages).
		If value is None, that means the "end of list" message was received.
		"""
		if value is not None:
			try:
				self._mode_lists[mode].add(value)
			except KeyError:
				self._mode_lists[mode] = [value]
			except AttributeError:
				self._mode_lists = {}
				self._mode_lists[mode] = [value]
		else:
			try:
				self.modes[mode] = self._mode_lists[mode]
			except (KeyError, AttributeError):
				self.modes[mode] = False

	def set_modes(self, modes):
		"""
		modes (dict) - the various modes set on the channel. Modes that are simply set or unset should be True/False;
		modes that take single arguments should be ints or strings; modes that take several arguments should be sets.
		"""
		self.modes.update(modes)
	
	def reset_list_modes(self):
		"""
		Clears type A channel modes in case the bot can no longer receive information on the mode (e.g. the bot lost ops). For use just before requesting type A mode info.
		482 ERR_CHANOPRIVSNEEDED messages only specify the mode in the msg part, which may vary.
		"""
		for mode in self.server.chanmodetypes['list']:
			self.modes[mode] = set()
	
	def update_creation_time(self, creation_time, UTC):
		"""
		time (int) - number of seconds since epoch as given by a 329 (RPL_CREATIONTIME) message.
		UTC (bool) - whether to UTC or local time
		"""
		self.creation_time = creation_time
		if UTC:
			self.creation_struct_time = time.gmtime(creation_time)
		else:
			self.creation_struct_time = time.localtime(creation_time)

class IRCMessage:
	def __init__(self, raw_bytes):
		self.args = []
		self.message = None
		message_list = []
		self.mask = None
		self.nick = None
		self.user = None
		self.cloak = None
		self.source = None # Set in handle() for relevant commands (PRIVMSG/NOTICE)
		self.raw_bytes = raw_bytes.rstrip()
		# b'9\xb0C/48\xb0F.'
		try:
			self.raw = self.raw_bytes.decode('utf-8')
		except UnicodeDecodeError:
			self.is_utf8 = False
			event_logger.warning('IRC message line could not be decoded with UTF-8:')
			event_logger.warning(self.raw_bytes)
			try:
				self.raw = self.raw_bytes.decode('unicode_escape')
			except UnicodeDecodeError:
				event_logger.exception('IRC message line could not be decoded with unicode_escape:')
				try:
					self.raw = self.raw_bytes.decode('raw_unicode_escape')
				except UnicodeDecodeError:
					event_logger.exception('IRC message line could not be decoded at all:')
					self.usable = False
					return self
		else:
			self.is_utf8 = True
		self.usable = True
		event_logger.debug(self.raw_bytes)
		sections = self.raw.split(' ')
		if sections[0][:1] != ":":
			self.origin = None
			self.command = sections.pop(0)
		else:
			self.origin = sections.pop(0)[1:]
			self.command = sections.pop(0)
			if '!' in self.origin:
				self.mask = self.origin
				self.nick, user_cloak  = self.mask.split('!')
				self.user, self.cloak = user_cloak.split('@')
		arg = True
		if sections:
			for section in sections:
				if arg:
					if section[:1] != ':':
						self.args.append(section)
					else:
						arg = False
						message_list = [section[1:]]
				else:
					message_list.append(section)
		self.args = ' '.join(self.args)
		if message_list:
			self.message = ' '.join(message_list)
		event_logger.debug('origin {} | mask {} | nick {} | user {} | cloak {} | command {} | args {} | message {}'.format(
						self.origin, self.mask, self.nick, self.user, self.cloak, self.command, self.args, self.message))

class NoiseBot:
	def __init__(self):
		self.alive = True
		self.load_configuration()
		self.configure_message_logging()
		self.setup()
		self.connect()
		self.handle_thread.start()
		self.ircd_recv_loop_thread.start()
		while self.alive:
			try:
				time.sleep(0.2)
			except KeyboardInterrupt as exc:
				self.die('Terminated at console', exc)
		try:
			self.killing_thread.join()
		except (RuntimeError, AttributeError):
			pass

	class die_if_error:
		def __init__(self, name):
			self.thread_name = name
	
		def __call__(self, func):
			def wrapper(bot_instance, *args, **kwargs):
				try:
					func(bot_instance, *args, **kwargs)
				except BaseException as exc:
					bot_instance.die('The {} thread died!'.format(self.thread_name), notify_owners=True, exc=exc)
			return wrapper

	def load_configuration(self):
		self.identity = types.SimpleNamespace(**id_conf)
		self.users = types.SimpleNamespace(**users_conf)
		self.network = types.SimpleNamespace(**net_conf)
		self.channels = types.SimpleNamespace(**chan_conf)
		self.modules = types.SimpleNamespace(**mod_conf)
		self.modules.module_names = set().union(self.modules.modules)
		self.modules.modules = {}
		self.network.network = ConnectedServer(network_name)
		self.network.detected_timeout = None
		self.identity.nicks = tuple(self.identity.preferred_nicks)
		self.identity.nick = self.identity.nicks[0]
		if self.identity.trigger in '.^$*+?|[]()\\':
			self.identity.escaped_trigger = r'\{0}'.format(self.identity.trigger)
		else:
			self.identity.escaped_trigger = self.identity.trigger
		self.identity.umodes = ''
		self.identity.is_oper = False
		self.network.ssl_version_ints = [ssl.__dict__[x] for x in self.network.ssl_protocols]
		self.channels.channels = {}
		self.identity.nick_attempt = 0
		self.identity.identified_at_startup = False
		self.users.owners_who_recv_notifs = set(self.users.owners_who_recv_notifs).intersection(set(self.users.owner_nicks)) # non-owners are stripped

	def configure_message_logging(self):
		self.log_str_to_level = msg_log_str_to_level
		logging.addLevelName(self.log_str_to_level['chat'], 'CHAT')
		logging.addLevelName(self.log_str_to_level['channel'], 'CHANNEL')
		logging.addLevelName(self.log_str_to_level['notice'], 'NOTICE')
		self.message_logs = types.SimpleNamespace(**msg_log_conf)
		self.message_log_rotation = types.SimpleNamespace(**msg_log_rotation_conf)
		self.message_logs.file_verbosity = self.log_str_to_level[self.message_logs.file_verbosity]
		self.message_logs.console_verbosity = self.log_str_to_level[self.message_logs.console_verbosity]
		self.message_logger = logging.getLogger('noisebot_messages')
		def make_log_function(level, *args, **kwargs):
			def log_function(message):
				if self.message_logger.isEnabledFor(level):
					self.message_logger._log(level, message, args, **kwargs)
			return log_function
		self.message_logger.chat = make_log_function(self.log_str_to_level['chat'])
		self.message_logger.channel = make_log_function(self.log_str_to_level['channel'])
		self.message_logger.notice = make_log_function(self.log_str_to_level['notice'])
		self.message_console_handler = logging.StreamHandler()
		if self.message_logs.rotate_logs:
			if self.message_log_rotation.timed:
				self.message_logs.opts = {
					"when": self.message_log_rotation.rollover_period,
					"interval": self.message_log_rotation.rollover_interval,
					"backupCount": self.message_log_rotation.max_logs,
					"encoding": self.message_logs.encoding,
					"delay": self.message_logs.delay_file_open,
					"utc": self.message_log_rotation.use_utc}
				self.message_file_handler = logging.handlers.TimedRotatingFileHandler(
					os.path.join(data_dir, 'networks', self.network.network.config_network_name, 'logs', 'status.log'), **self.message_logs.opts)
			else:
				self.message_logs.opts = {
					"mode": self.message_logs.write_mode,
					"maxBytes": self.message_log_rotation.max_bytes,
					"backupCount": self.message_log_rotation.max_logs,
					"encoding": self.message_logs.encoding,
					"delay": self.message_logs.delay_file_open}
				self.message_file_handler = logging.handlers.RotatingFileHandler(
					os.path.join(data_dir, 'networks', self.network.network.config_network_name, 'logs', 'status.log'), **self.message_logs.opts)
		else:
			self.message_logs.opts = {
					"mode": self.message_logs.write_mode,
					"encoding": self.message_logs.encoding,
					"delay": self.message_logs.delay_file_open}
			self.message_file_handler = logging.FileHandler(os.path.join(data_dir, 'networks', self.network.network.config_network_name, 'logs', 'status.log'), **self.message_logs.opts)
		#channel_message_file_handler = logging.handlers.TimedRotatingFileHandler(os.path.join(base_dir, chan, '.log'), encoding='utf-8')
		self.message_log_formatter = logging.Formatter(fmt=self.message_logs.formatting, datefmt=self.message_logs.time_formatting, style=self.message_logs.formatting_style)
		self.message_console_handler.setFormatter(self.message_log_formatter)
		self.message_file_handler.setFormatter(self.message_log_formatter)
		self.message_console_handler.setLevel(self.message_logs.console_verbosity)
		self.message_file_handler.setLevel(self.message_logs.file_verbosity)
		if self.message_logs.log_to_console:
			self.message_logger.addHandler(self.message_console_handler)
		if self.message_logs.log_to_file:
			self.message_logger.addHandler(self.message_file_handler)
		self.message_logger.setLevel(logging.DEBUG)
		self.message_logger.info('Message logger initialized.')
	
	def accept_module_connection(self, module_name, module):
		event_logger.debug('Establishing connection to {}...'.format(module_name))
		conn = self.module_listener.accept()
		module.connection = conn
	
	def register_module(self, module, message):
		if message['action'] != 'register':
			event_logger.error('Module registration function received a malformed/incorrect message.')
			event_logger.info(message)
			return False
		else:
			event_logger.debug(message)
			module.recv_events = message['recv_events']
			module.recv_commands = message['recv_commands']
			commands = message['commands']
			module.recv_message_types = message['recv_message_types']
			if not module.recv_commands:
				return True
			good_commands = set()
			commands_are_unique = True
			for command in commands:
				event_logger.debug('Processing the {0} command for {1}'.format(command, module.name))
				if command in self.commands:
					commands_are_unique = False
					if command in self.core_commands:
						event_logger.warning('Tried to register command "{0}" for module {1}, but it is already a core command.'.format(command, module.name))
						break
					for registered_module in self.modules.modules.values():
						if command in registered_module.commands:
							conflicting_module = registered_module
							event_logger.warning('Tried to register command "{0}" for module {1}, but it is already taken by {2}.'.format(command, module.name, conflicting_module.name))
							break
					break
				elif command in self.modules.modules.keys():
					event_logger.warning('Tried to register command "{0}" for module {1}, but it is a module name.'.format(command, module.name))
					commands_are_unique = False
					break
				else:
					event_logger.debug('Command {} was unique...'.format(command))
					good_commands = good_commands.union({command})
			if commands_are_unique:
				self.commands = self.commands.union(good_commands)
				module.commands = good_commands
				event_logger.debug('Good commands from {0} module: {1}'.format(module.name, good_commands))
				return True
			else:
				return False

	def unregister_module(self, module):
		module_name = module.name
		event_logger.debug(self.commands)
		event_logger.debug(module.commands)
		self.commands = self.commands.difference(module.commands)
		event_logger.debug(self.commands)
		module.die()
		try:
			del self.modules.modules[module_name]
		except KeyError:
			pass

	def kill_module_message_recv_thread(self):
		self.recv_module_messages = False
		try:
			self.recv_module_messages_thread.join(3)
		except RuntimeError:
			if self.recv_module_messages_thread.is_alive():
				event_logger.error('Module message recv thread could not be stopped.')
				return False
			else:
				return True
		else:
			return True			

	def start_new_module_message_recv_thread(self):
		"""
		Return True if a new thread was started, else return False.
		"""
		if not self.kill_module_message_recv_thread():
			return False
		self.recv_module_messages_thread = threading.Thread(target=self.recv_messages_from_modules, name='Module Message Receive Thread', daemon=True)
		self.recv_module_messages = True
		try:
			self.recv_module_messages_thread.start()
		except RuntimeError:
			event_logger.error('New module message recv thread could not be started.')
			self.recv_module_messages = False
			return False
		else:
			return True

	@die_if_error(name='Check Registration')
	def check_registration(self):
		while self.alive:
			check_registration_job = self.check_registration_jobs.get()
			self.msg(self.network.nick_service, 'status {}'.format(check_registration_job.nick))
			self.check_registration_jobs.task_done()
			self.pending_check_registration_jobs.append(check_registration_job)

	@die_if_error(name='Timeout Detection')
	def detect_timeout(self):
		sleep_time = self.network.ping_timeout / 2
		while self.network.network.time_of_last_ping is None:
			time.sleep(1)
		while self.alive:
			time.sleep(sleep_time)
			cur_time = time.time()
			event_logger.debug('Current time at timeout detection loop: {}'.format(cur_time))
			time_since_last_message = cur_time - self.network.network.time_of_last_message
			if time_since_last_message > self.network.ping_timeout:
				self.write('PING {}'.format(self.network.network.server_address))
				time.sleep(5)
				if not self.network.network.got_pong:
					event_logger.critical('Time since last message: {}'.format(time_since_last_message))
					self.die('Timed out!')
				else:
					self.network.network.got_pong = False

	def setup(self):
		self.admin_commands = frozenset(('join', 'part', 'partall', 'cycle', 'hop', 'quit', 'reloadmodule', 'loadmodule',
			'unloadmodule', 'raw', 'pm', 'msg', 'identify', 'invite', 'addowner', 'delowner', 'getowners',
			'getserverinfo', 'dumpchannelmodes', 'getchannels', 'getnicks', 'getmodules', 'getcommands', 'debugserverinfo', 'chanopstatus'))
		self.public_commands = frozenset(('help','usage','commands', 'modules', 'source', 'version'))
		self.commands = self.admin_commands.union(self.public_commands)
		self.core_commands = frozenset(self.commands)
		with open(os.path.join(global_data_dir, 'command_help.json')) as command_help_file:
			try:
				self.builtin_command_help_messages = json_decoder.decode(command_help_file.read())
			except ValueError:
				event_logger.error('The command help JSON file could not be decoded. This file should not typically be modified.')
				raise
		self.owner_notification_levels = {'off': 0, 'critical': 1, 'error': 2, 'warning': 3, 'info': 4}
		# Spawn, establish connections to, and register the specified modules
		self.incoming_messages = queue.Queue()
		self.message_dispatch_queue = queue.Queue()
		self.message_broadcast_queue = queue.Queue()
		self.check_registration_jobs = queue.Queue()
		self.pending_check_registration_jobs = collections.deque()
		self.finished_check_registration_jobs = queue.Queue(maxsize=1)
		self.ircd_recv_loop_thread = threading.Thread(target=self.ircd_recv_loop, name='IRCd Receive Thread', daemon=True)
		self.handle_thread = threading.Thread(target=self.handle, name='Handle Thread', daemon=True)
		self.timeout_detection_thread = threading.Thread(target=self.detect_timeout, name='Timeout Detection Thread', daemon=True)
		self.timeout_detection_thread.start()
		self.recv_module_messages_thread = threading.Thread(target=self.recv_messages_from_modules, name='Module Message Receive Thread', daemon=True)
		self.broadcast_event_thread = threading.Thread(target=self.broadcast_event, name='Event Broadcast Thread', daemon=True)
		self.broadcast_event_thread.start()
		self.broadcast_command_thread = threading.Thread(target=self.broadcast_command, name='Command Broadcast Thread', daemon=True)
		self.broadcast_command_thread.start()
		self.check_registration_thread = threading.Thread(target=self.check_registration, name='Registration Check Thread', daemon=True)
		try:
			self.module_listener = multiprocessing.connection.Listener(('localhost', self.modules.module_connection_port))
		except OSError:
			event_logger.error('Could not open a module listener socket. Try a different port?')
			self.die('Could not open module listener socket.', notify_owners=False)
		if self.modules.module_names:
			if not self.start_new_module_message_recv_thread():
				self.die('No module message recv thread.')
			#for module_name in self.modules.module_names:
			#	self.modules.modules[module_name] = None
			for module_name in self.modules.module_names:
				mod = self.load_module(module_name)
				if mod is not None:
					self.modules.modules[module_name] = mod
					self.start_module_connection(mod, False)
				else:
					if self.modules.exit_if_module_fails_to_load_at_startup:
						self.module_listener.close()
						event_logger.fatal('Could not load a module. Killing connections and exiting...')
						self.die('A module couldn\'t be loaded.')
					else:
						event_logger.warning('Could not load a module, but continuing anyway...')
						continue
		else:
			self.recv_module_messages = False

	def verify_pong(self, message):
		try:
			if message['action'] == 'pong_helper':
				return True
		except TypeError:
			event_logger.exception('Bad pong message type ({})'.format(message.__class__))
			return False
		except KeyError:
			event_logger.exception('Bad pong message JSON - expected {{"action": "pong_helper"}} but got {}'.format(message))
			return False

	def user_ops(self, nick, channel=None):
		if channel is None:
			return None
		nick_mode = None
		prefix_modes = channel.server.supported['prefix_by_mode'].keys()
		for mode in prefix_modes:
			event_logger.debug(mode)
			if nick in channel.modes[mode]:
				event_logger.debug('{0} has mode +{1}'.format(nick, mode))
				nick_mode = mode
				break
		if nick_mode is None:
			return None
		else:
			return channel.server.prefix_mode_names[nick_mode]

	def user_is_owner(self, nick, cloak):
		cloak_matches = cloak in self.users.owner_cloaks and self.users.use_cloak_auth
		nick_matches = nick in self.users.owner_nicks and self.users.use_nick_auth
		if cloak_matches:
			return True
		if nick_matches:
			reg_job = RegistrationCheckJob(self.network.network, nick)
			self.check_registration_jobs.put(reg_job)
			try:
				reg_job = self.finished_check_registration_jobs.get(block=True, timeout=5)
			except queue.Empty:
				event_logger.warning('Tried to check registration for {} but did not receive a timely response.'.format(reg_job.nick))
				return False
			try:
				return reg_job.nick_is_registered
			except AttributeError:
				self.message_logger.error('Could not determine registration status for {}'.format(reg_job.nick))
				return False
			finally:
				self.finished_check_registration_jobs.task_done()
		return False

	def process_public_command(self, irc_message, command):
		message_with_command = irc_message.message
		message_list = message_with_command.split(' ')
		del message_list[0]
		message = ' '.join(message_list)
		source = irc_message.source
		nick = irc_message.nick
		if command in ('help', 'usage'):
			if not message:
				self.msg(source, "Type '{0}commands' for a list of commands or '{0}help <command>' to get help for a particular command.".format(self.identity.trigger))
				if command == 'help':
					self.msg(source, 'See https://code.google.com/p/noisebot/ for source code and more information.')
				return
			try:
				help_message, usage = self.builtin_command_help_messages[message]
			except KeyError:
				if message in self.commands:
					modules = frozenset(self.modules.modules.values())
					for module in modules:
						if (message in module.commands) and module.recv_commands:
							self.send_irc_message_to_module(module, command, irc_message)
				elif message in self.modules.modules.keys():
					irc_message.message = None
					self.send_irc_message_to_module(self.modules.modules[message], command, irc_message)
				else:
					self.notice('{} is not a recognized command.'.format(message), nick)
					return
			else:
				usage = 'Usage: {}'.format(usage.format(self.identity.trigger))
				if command == 'help':
					self.msg(source, help_message)
				self.msg(source, usage)
		elif command == 'commands':
			if not message:
				if self.modules.modules:
					self.msg(source, "Type '{0}commands <module>' to get the commands for a module or '{0}commands core' for a list of built-in commands.".format(self.identity.trigger))
					self.msg(source, 'Available modules: {}'.format(' | '.join(self.modules.modules.keys())))
					return
				else:
					commands_to_display = self.public_commands
					message = 'core'
			elif message == 'core':
				commands_to_display = self.public_commands
				message = 'core'
			elif message == 'admin':
				commands_to_display = self.admin_commands
			else:
				try:
					commands_to_display = self.modules.modules[message].commands
				except KeyError:
					self.msg(source, "{0} is not an available module. Type '{1}modules' for a list of available modules.".format(message, self.identity.trigger))
					return
				except AttributeError:
					self.msg(source, '{0} is currently being loaded. Try again in a few seconds.'.format(message))
					return
			commands_to_display = list(commands_to_display)
			commands_to_display.sort()
			self.msg(source, 'Available {0} commands: {1}'.format(message, ', '.join(commands_to_display)))
		elif command == 'modules':
			if self.modules.modules:
				self.msg(source, 'Available modules: {}'.format(' | '.join(self.modules.modules.keys())))
			else:
				self.msg(source, 'No modules are loaded.')
		elif command in ('source', 'version'):
			if message:
				try:
					module_message = self.modules.modules[message].__dict__[command]
				except KeyError:
					self.msg(source, "{0} is not an available module. Type '{1}modules' for a list of available modules.".format(message, self.identity.trigger))
				except AttributeError:
					self.msg(source, '{} is currently being loaded. Try again in a few seconds.'.format(message))
				else:
					if module_message:
						self.msg(source, module_message)
			elif not message or message.lower == 'noisebot':
				if command == 'version':
					self.msg(source, 'NoiseBot version: {}'.format(VERSION))
				elif command == 'source':
					self.msg(source, SOURCE_URL)

	def process_admin_command(self, nick, source, command, message):
		if command == 'join':
			if message:
				self.join(message)
			else:
				self.notice('No channel specified.', nick)
		elif command == 'part':
			if source[:1] in self.network.network.chantypes:
				if message:
					if message[:1] in self.network.network.chantypes:
						msg_list = message.split()
						channel = msg_list[0]
						message = ' '.join(msg_list[1:])
					else:
						channel = source
					self.part(channel, message)
				else:
					self.part(source)
			else:
				if message:
					if message[:1] in self.network.network.chantypes:
						self.part(message)
					else:
						self.notice('No channel specified', nick)
				else:
					self.notice('No channel specified', nick)
		elif command == 'partall':
			for channel_name in self.channels.channels.keys():
				self.part(channel_name, '')
		elif command in ('cycle', 'hop'):
			if source[:1] in self.network.network.chantypes:
				if message:
					if message[:1] in self.network.network.chantypes:
						msg_list = message.split()
						channel = msg_list[0]
						message = ' '.join(msg_list[1:])
					else:
						channel = source
					self.part(channel, message)
				else:
					channel = source
					self.part(channel)
				self.join(channel)
			else:
				if message:
					self.part(message)
					self.join(message)
				else:
					self.notice('No channel specified', nick)
		elif command == 'quit':
			self.users.owners_who_recv_notifs.discard(nick)
			self.disconnect(reason='{} sent the quit command.'.format(nick), msg=message)
		elif command == 'reloadmodule':
			if message:
				if message in self.modules.modules.keys():
					module = self.reload_module(self.modules.modules[message], '{} initiated reload.'.format(nick))
					if module is not None:
						self.modules.modules[module.name] = module
						self.start_module_connection(module)
						self.notice('{} module reloaded'.format(module.name), nick)
					else:
						self.notice('The {} module could not be loaded.'.format(message), nick)
				else:
					self.notice('{} module is not loaded.'.format(message), nick)
			else:
				self.notice('No module specified.', nick)
		elif command == 'loadmodule':
			event_logger.debug('modules before loading - {}'.format(self.modules.modules.keys()))
			if message:
				if message in self.modules.modules.keys():
					self.notice('The {} module is already loaded.'.format(message), nick)
				else:
					module = self.load_module(message)
					if module is not None:
						self.modules.modules[module.name] = module
						self.notice('The {} module is now loaded.'.format(module.name), nick)
						self.start_module_connection(module)
					else:
						self.notice('The {} module could not be loaded.'.format(message), nick)
					event_logger.debug('modules after loading - {}'.format(self.modules.modules.keys()))
			else:
				self.notice('No module specified.', nick)
		elif command == 'unloadmodule':
			event_logger.debug('modules before unloading - {}'.format(self.modules.modules.keys()))
			if message:
				if message in self.modules.modules.keys():
					self.unload_module(self.modules.modules[message], '{} initiated unload.'.format(nick))
					if message in self.modules.modules.keys():
						self.notice('{} module could not be unloaded. Check terminal.'.format(message), nick)
					else:
						self.notice('{} module is unloaded.'.format(message), nick)
				else:
					self.notice('Module is not loaded.', nick)
			else:
				self.notice('No module specified.', nick)
		elif command == 'raw':
			if message:
				self.write(message)
			else:
				self.notice('No message to send.', nick)
		elif command in ('pm', 'msg'):
			if message:
				message_list = message.split(' ')
				dest = message_list.pop(0)
				message = ' '.join(message_list)
				if message is not None:
					self.msg(dest, message)
				else:
					self.notice('No message to send.', nick)
			else:
				self.notice('No message to send.', nick)
		elif command == 'identify':
			if message:
				self.identify_with_services(message)
			else:
				self.notice('No password specified.', nick)
		elif command == 'invite':
			if message:
				message_list = message.split(' ')
				invitee = message_list.pop(0)
				try:
					channel_name = message_list[0]
					if channel_name[:1] not in self.network.network.chantypes:
						self.notice('{} is not a valid channel.'.format(channel_name), nick)
						return
				except IndexError:
					if source[:1] not in self.network.network.chantypes:
						self.msg(nick, 'No invitee specified.')
						return
					else:
						channel_name = source
				self.invite(invitee, channel_name)
			else:
				self.notice('No invitee specified.', nick)
		elif command == 'addowner':
			if message:
				new_owner_list = message.split(' ')
				for new_owner in new_owner_list:
					if '.' in new_owner and '@' not in new_owner and '!' not in new_owner:
						if new_owner in self.users.owner_cloaks:
							self.notice('{} is already an owner cloak.'.format(new_owner), nick)
						else:
							self.users.owner_cloaks.append(new_owner)
							self.notice('{} added.'.format(new_owner), nick)
					else:
						for char in '.@!':
							if char in new_owner:
								self.notice('{} is not a valid cloak or nick.'.format(new_owner), nick)
								return
						self.users.owner_nicks.append(new_owner)
						self.notice('{} added.'.format(new_owner), nick)
			else:
				self.notice('No cloak specified.', nick)
		elif command == 'delowner':
			if message:
				if message in self.users.owner_cloaks:
					self.users.owner_cloaks.remove(message)
					self.notice('{} removed.'.format(message), nick)
				elif message in self.users.owner_nicks:
					self.users.owner_nicks.remove(message)
					self.notice('{} removed.'.format(message), nick)
				else:
					self.notice('{} is not an owner cloak or nick.'.format(message), nick)
			else:
				self.notice('No cloak specified.', nick)
		elif command == 'getowners':
			owner_cloaks = ' '.join(self.users.owner_cloaks)
			owner_nicks = ' '.join(self.users.owner_nicks)
			self.notice(owner_cloaks, nick)
			self.notice(owner_nicks, nick)
		elif command == 'getserverinfo':
			print('Features supported by the IRCd:')
			pprint.pprint(self.network.network.supported)
			print('Channel mode types supported by the IRCd:')
			pprint.pprint(self.network.network.chanmodetypes)
			print('Channel modes supported by the IRCd:')
			pprint.pprint(self.network.network.chanmodes)
			self.notice('Server info dumped to terminal.', nick)
		elif command == 'dumpchannelmodes':
			if message:
				channel_name = message
			else:
				if source[:1] in self.network.network.chantypes:
					channel_name = source
				else:
					self.notice('Not in a channel and no channel specified.', nick)
					return None
			try:
				pprint.pprint(self.channels.channels[channel_name].modes)
			except KeyError:
				self.notice('Not in {}.'.format(channel_name), nick)
			else:
				self.notice('Dumped channel modes to terminal.', nick)
		elif command == 'getchannels':
			self.msg(source, ' '.join(self.channels.channels.keys()))
		elif command == 'getnicks':
			if message:
				channel_name = message
			else:
				if source[:1] in self.network.network.chantypes:
					channel_name = source
				else:
					self.notice('Not in a channel and no channel specified.', nick)
					return None
			try:
				nicks = self.channels.channels[channel_name].nicks
			except KeyError:
				self.notice('Not in {}.'.format(channel_name), nick)
			else:
				self.msg(source, ' '.join(nicks))
		elif command == 'getmodules':
			print(' | '.join(self.modules.modules.keys()))
		elif command == 'getcommands':
			pprint.pprint(self.commands)
		elif command == 'debugserverinfo':
			pprint.pprint(self.network.network.export_state())
		elif command == 'chanopstatus':
			if message:
				message_list = message.split(' ')
				target_nick = message_list[0]
				try:
					channel_name = message_list[1]
				except IndexError:
					if source[:1] in self.network.network.chantypes:
						channel_name = source
					else:
						self.notice('Not in a channel and no channel specified.', nick)
						return
				channel = self.channels.channels[channel_name]
				if target_nick not in channel.nicks:
					self.notice('{0} is not in {1}.'.format(target_nick, channel.name), nick)
					return
				chanop_privs = self.user_ops(target_nick, channel)
				if chanop_privs is None:
					self.msg(source, '{0} has no channel op privileges on {1}'.format(target_nick, channel_name))
				else:
					self.msg(source, '{0} has {1} privileges on {2}'.format(target_nick, chanop_privs, channel_name))
			else:
				self.notice('No nick specified.', nick)

	def reload_module(self, module, reason=''):
		module_name = module.name
		self.unload_module(module, reason)
		return self.load_module(module_name)
	
	def unload_module(self, module, reason=''):
		self.send_die_message_to_module(module, reason)
		try:
			self.unregister_module(self.modules.modules[module.name])
		except KeyError:
			pass
		if not self.modules.modules:
			if self.kill_module_message_recv_thread():
				event_logger.info('Module message recv thread was joined.')
			else:
				self.die('Couldn\'t stop module message recv thread.')
	
	def load_module(self, module_name, connected_to_network=True):
		event_logger.debug('Loading {} module'.format(module_name))
		if not self.recv_module_messages_thread.is_alive():
			if self.start_new_module_message_recv_thread():
				event_logger.debug('Started new module message recv thread.')
			else:
				self.die('Test fail - no new module message recv thread.')
		else:
			event_logger.debug('Module message recv thread is alive.')
		module = Module(module_name)
		try:
			module.load_config()
		except OSError:
			event_logger.exception('Could not load module config:')
			module.die()
			return None
		module_connection_thread = threading.Thread(target=self.accept_module_connection, name='Accept Module Connection Thread ({})'.format(module.name), args=(module_name, module), daemon=True)
		try:
			module.spawn()
		except FileNotFoundError:
			event_logger.exception('Could not spawn module:')
			return None
		module_connection_thread.start()
		module_connection_thread.join(self.modules.module_connection_timeout)
		if module_connection_thread.is_alive():
			event_logger.warning('Could not connect to {}.'.format(module_name))
			module.die()
			dummy_conn = multiprocessing.connection.Client(('localhost', self.modules.module_connection_port))
			dummy_conn.close()
			if module_connection_thread.is_alive():
				event_logger.critical('Could not connect to {} and the dummy connection didn\'t kill the thread.'.format(module_name))
			return None
		else:
			event_logger.debug('Started new {} module connection and thread is dead.'.format(module.name))
		if module.connection.poll(5):
			event_logger.debug('module.connection.poll returned True.')
			temp_recv_thread = threading.Thread(target=module.queue_temp_recv_message, name='Temporary Module Receive Thread ({})'.format(module.name), daemon=True)
			temp_recv_thread.start()
			try:
				register_message = module.temp_connection_recv_queue.get(timeout=5)
			except queue.Empty:
				event_logger.warning('The connection to the {} module blocked.'.format(module.name))
				module.die()
				return None
			try:
				register_message_dict = json_decoder.decode(register_message)
			except EOFError:
				event_logger.warning('The connection to the {} module was closed unexpectedly during registration.'.format(module.name))
				module.die()
				return None
			except ValueError:
				event_logger.warning('Received invalid JSON from {} module during registration.'.format(module))
				module.die()
				return None
			except ConnectionError:
				event_logger.warning('The connection to the {} module was terminated unexpectedly during registration.'.format(module))
				module.die()
				return None
			except:
				event_logger.exception('Unexpected error during registration of the {} module:'.format(module.name))
				module.die()
				return None
			else:
				event_logger.debug('Got register message.')
				module.is_responsive = True
			if self.register_module(module, register_message_dict):
				event_logger.debug('Loaded and registered {} module.'.format(module.name))
				return module
			else:
				event_logger.warning('The {} module couldn\'t be registered.'.format(module.name))
				module.die()
				return None
		else:
			if self.modules.exit_if_module_fails_to_load_at_startup:
				event_logger.fatal('The {} module could not be registered. Killing connections and exiting...'.format(module.name))
				self.module_listener.close()
				module.die()
				self.die('A module couldn\'t be registered.')
			else:
				event_logger.warning('The {} module could not be registered, but continuing anyway...'.format(module.name))
				self.module_listener.close()
				return None

	def start_module_connection(self, module, connected_to_network=True):
		module.connection_thread.start()
		self.broadcast_module_info()
		if connected_to_network:
			self.broadcast_server_info(self.network.network, module)
			for channel in self.channels.channels.values():
				self.broadcast_channel_info(channel, module)

	def die(self, reason, notify_owners=True, exc=None):
		"""
		Kill modules, join threads, and exit.
		"""
		event_logger.fatal(reason)
		if notify_owners:
			self.notify_owners_of_error('critical', reason)
		modules = frozenset(self.modules.modules.values())
		for module in modules:
			if module is not None:
				self.send_die_message_to_module(module, 'Bot is shutting down.')
				module.die()
		self.killing_thread = threading.current_thread()
		self.alive = False
		try:
			self.irc.close()
		except AttributeError:
			# if the connection hasn't been opened yet, this spews out a useless traceback
			pass
		if isinstance(exc, BaseException):
			try:
				raise exc
			except (KeyboardInterrupt, SystemExit):
				pass
			except:
				event_logger.exception('This exception terminated the bot:')
				raise
		elif isinstance(exc, type(None)):
			pass
		else:
			event_logger.error('The bot was shut down by an exception, but received {} instead of an exception.'.format(exc))
		sys.exit()

	def connect_ssl(self, ssl_version_int):
		self.irc = ssl.wrap_socket(socket.socket(), ssl_version=ssl_version_int)
		try:
			self.irc.connect((self.network.address, self.network.ssl_port))
		except ssl.SSLError:
			event_logger.exception('SSL connection failed:')
			return False
		except Exception:
			event_logger.exception('Unexpected error during SSL connection:')
			self.die('Could not connect.')
		else:
			return True

	def connect_non_ssl(self):
		self.irc = socket.socket()
		self.irc.connect((self.network.address, self.network.non_ssl_port))
		
	def connect(self):
		if self.network.prefer_ssl:
			connected = False
			for ssl_version_int in self.network.ssl_version_ints:
				if self.connect_ssl(ssl_version_int):
					connected = True
					break
			if not connected:
				if self.network.ssl_only:
					self.die('Could not connect via SSL.')
				else:
					self.connect_non_ssl()
		else:
			self.connect_non_ssl()

	# Send a raw message to the IRC server.
	def write(self, message):
		message = message.replace('\n','').replace('\r','')
		self.message_logger.debug('Outgoing message: {}'.format(message))
		message += '\r\n'
		#message = bytes(message, 'raw_unicode_escape')
		message = bytes(message, 'utf-8')
		self.irc.send(message)

	def msg(self, dest, message):
		self.write('PRIVMSG {0} :{1}'.format(dest, message))
		self.message_logger.info('PRIVMSG sent to {0}: {1}'.format(dest, message))
	
	def notice(self, message, dest):
		self.write('NOTICE {0} :{1}'.format(dest, message))
		self.message_logger.info('NOTICE sent to {0}: {1}'.format(dest, message))
	
	def disconnect(self, reason, msg=None):
		self.notify_owners_of_error('critical', reason)
		if not msg:
			msg = 'Bye'
		self.write('QUIT :{}'.format(msg))

	# Invite a nick to the specified channel
	def invite(self, nick, channel_name):
		self.write('INVITE {0} {1}'.format(nick, channel_name))

	## Reply to server PINGs.
	def pong(self):
		self.write('PONG :{}'.format(self.message.message))
		self.message_logger.debug('Sent PONG to {}'.format(self.message.message))

	## Join channel(s).
	def join(self, channel_names_and_keys):
		self.write('JOIN ' + channel_names_and_keys)

	def mode(self, target, mode=None):
		"""
		Sets or gets a mode for a channel or user (target).
		"""
		if mode is None:
			self.write('MODE {0}'.format(target))
		else:
			self.write('MODE {0} {1}'.format(target, mode))
	
	def part(self, channel_name, message=None):
		if message is not None:
			self.write('PART {0} :{1}'.format(channel_name, message))
		else:
			self.write('PART {0}'.format(channel_name))
	
	def topic(self, channel_name, message=None):
		if message is None:
			self.write('TOPIC {}'.format(channel_name))
		else:
			self.write('TOPIC {0} :{1}'.format(channel_name, message))

	def get_names(self, channel_name):
		self.write('NAMES {}'.format(channel_name))

	## Identify to the server (NICK and USER commands).
	def identify(self, nick):
		self.write('NICK {}'.format(nick))
		self.write('USER {0} 3 * :{1}'.format(self.identity.user, self.identity.name))

	## Identify to services if a password is defined in the configuration.
	def identify_with_services(self, password, service_nick='nickserv'):
		self.msg(service_nick, 'identify {}'.format(password))

	def get_channel_modes(self, channel_name, modes=None):
		"""
		This method only requests the information from the server - handle() processes incoming information.
		channel (string) is the channel.
		modes (iterable - typically a string) of modes to get.
		"""
		if modes is None:
			modes = self.network.network.chanmodetypes['list']
		for mode in modes:
			self.mode(channel_name, mode)
	
	def update_channel_info(self, channel_name):
		self.get_channel_modes(channel_name)
		self.get_names(channel_name)

	## Main receiving loop.
	@die_if_error(name='IRCd Receive Loop')
	def ircd_recv_loop(self):
		last_line = None
		lines = collections.deque()
		while self.alive:
			## Incoming data
			try:
				data = self.irc.recv(1024)
			except ConnectionAbortedError:
				if self.alive:
					raise
				else:
					event_logger.debug('Ignored ConnectionAbortedError...')
			if not data:
				continue
			if last_line is not None:
				self.message_logger.debug('Appended last line!')
				self.message_logger.debug(data)
				data = last_line + data
			self.message_logger.debug(data)
			lines.extend(data.splitlines(True))
			last_line = lines.pop()
			self.network.network.time_of_last_message = time.time()
			while True:
				try:
					line = lines.popleft()
				except IndexError:
					break
				else:
					self.incoming_messages.put(line.rstrip(b'\r\n'))
			if last_line.endswith(b'\r\n'):
				self.incoming_messages.put(last_line.rstrip(b'\r\n'))
				last_line = None
				
	
	def get_channels(self):
		event_logger.debug(' '.join(self.channels.channels.keys()))

	def remove_channel_from_list(self, channel_name):
		try:
			del self.channels.channels[channel_name]
		except KeyError:
			# This happens if an invalid channel is specified. Typically, if not always, the result of a bug.
			event_logger.error('Tried to remove {} from channels list, but it wasn\'t there!'.format(channel_name))
	

	def dump_state(self):
		"""
		self.identity = types.SimpleNamespace(**id_conf)
		self.users = types.SimpleNamespace(**users_conf)
		self.network = types.SimpleNamespace(**net_conf)
		self.channels = types.SimpleNamespace(**chan_conf)
		self.modules = types.SimpleNamespace(**mod_conf)
		self.message_logs = types.SimpleNamespace(**msg_log_conf)
		self.message_log_rotation = types.SimpleNamespace(**msg_log_rotation_conf)
		"""
		state = {}
		state['identity'] = self.identity.__dict__
		state['users'] = self.users.__dict__
		state['network'] = self.network.__dict__
		state['channels'] = self.channels.__dict__
		state['modules'] = self.modules.__dict__
		state['message_logs'] = self.message_logs.__dict__
		state['message_log_rotation'] = self.message_log_rotation.__dict__
		pprint.pprint(state)
	
	def get_list_from_numeric_args(self, args):
		"""Returns a list from the args of a numeric reply without the destination (the bot's nick)"""
		args_list = args.split(' ')
		del args_list[0]
		return args_list

	@die_if_error(name='Handle')
	def handle(self):
		"""
		Incoming IRC messages are processed here.
		line is a raw IRC message (a bytes object).
		
		https://www.alien.net.au/irc/irc2numerics.html
		https://tools.ietf.org/html/rfc2812
		https://tools.ietf.org/html/rfc1459
		http://wiki.inspircd.org/List_Of_Numerics (InspIRCd)
		"""
		while self.alive:
			line = self.incoming_messages.get()
			self.message = IRCMessage(line)
			if not self.message.usable:
				self.notify_owners_of_error('error', 'A message couldn\'t be decoded. See logs.')
				self.incoming_messages.task_done()
				return
			if self.message.nick in self.users.ignored_nicks or self.message.cloak in self.users.ignored_cloaks:
				self.message_logger.debug('Not sending message from {} to modules'.format(self.message.origin))
				send_message_to_modules = False
			else:
				send_message_to_modules = True
			message_is_valid = True
			if self.message.command == '001':
				# 001 RPL_WELCOME (RFC2812) "Welcome to the IRC network {mask}"
				command_name = '001 RPL_WELCOME'
				self.message_logger.notice('[{0}] {1}'.format(command_name, self.message.message))
				if self.identity.identify_to_services:
					self.identify_with_services(self.identity.services_pass, self.network.nick_service)
				self.identity.nick = self.message.args
				self.check_registration_thread.start()
				#time.sleep(2)
			elif self.message.command == '002':
				# 002 RPL_YOURHOST (RFC2812) "Your host is {server} running {ircd}"
				command_name = '002 RPL_YOURHOST'
				self.message_logger.notice('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '003':
				# 003 RPL_CREATED (RFC2812) "This server was created {date_and_time}"
				command_name = '003 RPL_CREATED'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '004':
				# 004 RPL_MYINFO (RFC2812) Server information
				# At least KineIRCd uses a different format; receiving 004 from such an IRCd will likely raise an exception.
				command_name = '004 RPL_MYINFO'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.args))
				self.network.network.get_info_from_RPL_MYINFO(self.get_list_from_numeric_args(self.message.args))
			elif self.message.command == '005':
				# 005 RPL_ISUPPORT / 005 RPL_PROTOCTL (InspIRCd, Bahamut, Unreal, Ultimate, ???) - Detailed ircd info (modes, channel types, etc.)
				# 005 RPL_BOUNCE (RFC2812, ???) "Try server {server}"
				command_name = '005 RPL_ISUPPORT'
				self.message_logger.info('[{0}] {1} {2}'.format(command_name, self.message.args, self.message.message))
				self.network.network.get_info_from_RPL_ISUPPORT(self.get_list_from_numeric_args(self.message.args))
				if self.channels.autojoin_channels:
					time.sleep(1)
					self.join(self.channels.autojoin_channels)
			elif self.message.command == '006':
				# 006 RPL_MAP (Unreal, InspIRCd, ???)
				command_name = '006 RPL_MAP'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '007':
				# 007 RPL_MAPEND (Unreal, InspIRCd, ???)
				command_name = '007 RPL_MAPEND'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '042':
				# 042 RPL_YOURID (InspIRCd, IRCnet, ???) "{id} your unique ID"
				command_name = '042 RPL_YOURID'
				self.message_logger.info('[{0}] {1} {2}'.format(command_name, self.message.args.split(' ')[1], self.message.message))
				self.broadcast_server_info(self.network.network)
			elif self.message.command == '221':
				# 221 RPL_UMODEIS (RFC 1459)
				command_name = '221 RPL_UMODEIS'
				if self.message.message is not None:
					modes = self.message.message[1:]
					user = self.message.args
				else:
					user, modes = self.message.args.split(' ')
					modes = modes[1:]
				if self.identity.nick == user:
					self.identity.umodes = modes
					self.broadcast_server_info(self.network.network)
				self.message_logger.info('[{0}] {1} has mode +{2}'.format(command_name, user, modes))
			elif self.message.command == '250':
				# 250 RPL_STATSCONN (ircu, Unreal, ???)
				# 250 RPL_STATSDLINE (RFC 2812)
				command_name = '250 RPL_STATSDLINE'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '251':
				# 251 RPL_LUSERCLIENT (RFC 1459)
				command_name = '251 RPL_LUSERCLIENT'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command in ('252', '253', '254'):
				if self.message.command == '252':
					# 252 RPL_LUSEROP (RFC 1459)
					command_name = '252 RPL_LUSEROP'
				if self.message.command == '253':
					# 253 RPL_LUSERUNKNOWN (RFC 1459)
					command_name = '253 RPL_LUSERUNKNOWN'
				if self.message.command == '254':
					# 254 RPL_LUSERCHANNELS (RFC 1459)
					command_name = '254 RPL_LUSERCHANNELS'
				self.message_logger.info('[{0}] {1} {2}'.format(command_name, self.message.args.split(' ')[1], self.message.message))
			elif self.message.command == '255':
				# 255 RPL_LUSERME (RFC 1459)
				command_name = '255 RPL_LUSERME'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '265':
				# 265 RPL_LOCALUSERS (aircd, Hybrid, Hybrid, Bahamut, InspIRCd, ???)
				command_name = '265 RPL_LOCALUSERS'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '266':
				# 266 RPL_GLOBALUSERS (aircd, Hybrid, Hybrid, Bahamut, InspIRCd, ???)
				command_name = '266 RPL_GLOBALUSERS'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '307':
				# 307 RPL_USERIP (???)
				# 307 RPL_WHOISREGNICK (InspIRCd, hybrid, Bahamut, Unreal)
				# 307 RPL_SUSERHOST (AustHex)
				command_name = '307 RPL_WHOISREGNICK'
				nick = self.get_list_from_numeric_args(self.message.args)[0]
				self.message_logger.info('[{0}] {1} {2}'.format(command_name, nick, self.message.message))
			elif self.message.command == '324':
				# 324 RPL_CHANNELMODEIS (RFC 1459)
				# (Channel modes that do not have a list as a parameter)
				command_name = '324 RPL_CHANNELMODEIS'
				args_list = self.get_list_from_numeric_args(self.message.args)
				channel_name = args_list.pop(0)
				channel = self.channels.channels[channel_name]
				params = args_list
				self.message_logger.channel('[{0}] {1} has modes: {2}'.format(command_name, channel_name, ' '.join(params)))
				channel_modes = params[0][1:]
				# process until arguments are gone
				while len(params) > 1:
					current_mode = channel_modes[0]
					if current_mode in self.network.network.server_chan_modes_args:
						value = params.pop(1)
						channel.set_modes({current_mode:value})
					else:
						channel.set_modes({current_mode:True})
					channel_modes = channel_modes[1:]
				# process any modes that are left
				for current_mode in channel_modes:
					channel.set_modes({current_mode:True})
			elif self.message.command == '329':
				# 329 RPL_CREATIONTIME
				command_name = '329 RPL_CREATIONTIME'
				channel_name, creation_time = self.get_list_from_numeric_args(self.message.args)
				channel = self.channels.channels[channel_name]
				channel.update_creation_time(int(creation_time), self.message_logs.use_utc)
				self.message_logger.info('[{0}] {1} was created {2}'.format(command_name, channel.name,
					time.strftime(self.message_logs.creation_time_format, channel.creation_struct_time)))
				self.broadcast_channel_info(channel)
			#elif self.message.command == '330':
				# 330 RPL_WHOISACCOUNT (InspIRCd, hybrid, ircu)
				# 330 RPL_WHOWAS_TIME (???)
			#	pass
			elif self.message.command == '332':
				# 332 RPL_TOPIC (RFC 1459)
				command_name = '332 RPL_TOPIC'
				channel_name = self.message.args.split(' ')[1]
				channel = self.channels.channels[channel_name]
				self.message_logger.channel('[{0}] Topic for {1} is: {2}'.format(command_name, channel.name, self.message.message))
				channel.topic = self.message.message
				try:
					self.broadcast_channel_info(channel)
				except AttributeError:
					pass
			elif self.message.command == '333':
				# 333 RPL_TOPICWHOTIME (InspIRCd, ircu, ???)
				command_name = '333 RPL_TOPICWHOTIME'
				args_list = self.get_list_from_numeric_args(self.message.args)
				if self.message_logs.use_utc:
					topic_time = time.strftime(self.message_logs.topic_time_format, time.gmtime(float(args_list[2])))
				else:
					topic_time = time.strftime(self.message_logs.topic_time_format, time.localtime(float(args_list[2])))
				self.message_logger.channel('[{0}] Topic for {1} was set by {2} on {3}'.format(command_name, args_list[0], args_list[1], topic_time))
			elif self.message.command == '341':
				# 341 RPL_INVITING (RFC 1459)
				command_name = '341 RPL_INVITING'
				args_list = self.message.args.split(' ')
				self.message_logger.notice('[{0}] {1} invited {2} into {3}'. format(command_name, *args_list))
			elif self.message.command == '346':
				# 346 RPL_INVITELIST (RFC 2812)
				command_name = '346 RPL_INVITELIST'
				args_list = self.get_list_from_numeric_args(self.message.args)
				while len(args_list) < 4:
					args_list.append(None)
				channel_name, invited_mask, inviter, time_of_invite = args_list
				self.channels.channels[channel_name].rebuild_mode_list('I', {'invited':invited_mask,'inviter':inviter,'time_of_invite':time_of_invite})
				self.message_logger.channel('[{0}] {1} may join {2} without an invite'.format(command_name, invited_mask, channel_name))
			elif self.message.command == '347':
				# 347 RPL_ENDOFINVITELIST (RFC 2812)
				command_name = '347 RPL_ENDOFINVITELIST'
				channel_name = self.message.args.split(' ')[1]
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('I', None)
				self.message_logger.info('[{0}] {1} for {2}'.format(command_name, self.message.message, channel.name))
				try:
					self.broadcast_channel_info(channel)
				except AttributeError:
					pass
			elif self.message.command == '348':
				# 348 RPL_EXCEPTLIST / RPL_EXLIST (RFC 2812)
				command_name = '348 RPL_EXCEPTLIST'
				args_list = self.get_list_from_numeric_args(self.message.args)
				while len(args_list) < 4:
					args_list.append(None)
				channel_name, exempt_mask, exempted_by, time_of_exemption = args_list
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('e', {'exempt_mask':exempt_mask,'exempted_by':exempted_by,'time_of_exemption':time_of_exemption})
				self.message_logger.channel('[{0}] {1} is exempt from bans on {2}'.format(command_name, exempt_mask, channel_name))
			elif self.message.command == '349':
				# 349 RPL_ENDOFEXCEPTLIST / RPL_ENDOFEXLIST (RFC 2812)
				command_name = '349 RPL_ENDOFEXCEPTLIST'
				channel_name = self.message.args.split(' ')[1]
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('e', None)
				self.message_logger.channel('[{0}] {1} for {2}'.format(command_name, self.message.message, channel.name))
				self.broadcast_channel_info(channel)
			elif self.message.command == '353':
				# 353 RPL_NAMREPLY (RFC 1459)
				command_name = '353 RPL_NAMREPLY'
				channel_name = self.message.args.split(' ')[2]
				channel = self.channels.channels[channel_name]
				self.message_logger.info('[{0}] Names for {1}: {2}'.format(command_name, channel.name, self.message.message))
				names_list = self.message.message.split(' ')
				channel.add_names(names_list)
			elif self.message.command == '366':
				# 366 RPL_ENDOFNAMES (RFC 1459)
				command_name = '366 RPL_ENDOFNAMES'
				channel_name = self.message.args.split(' ')[1]
				self.message_logger.info('[{0}] {1} ({2})'.format(command_name, self.message.message, channel_name))
				channel = self.channels.channels[channel_name]
				channel.update_names()
				try:
					self.broadcast_channel_info(channel)
				except AttributeError:
					pass
			elif self.message.command == '367':
				# 367 RPL_BANLIST (RFC 1459)
				command_name = '367 RPL_BANLIST'
				args_list = self.get_list_from_numeric_args(self.message.args)
				channel_name, banned_mask, banned_by, time_of_ban = args_list
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('b', {'banned_mask':banned_mask,'banned_by':banned_by,'time_of_ban':time_of_ban})
				self.message_logger.channel('[{0}] {1} is banned from {2}'.format(command_name, banned_mask, channel.name))
			elif self.message.command == '368':
				# 368 RPL_ENDOFBANLIST (RFC 1459)
				command_name = '368 RPL_ENDOFBANLIST'
				channel_name = self.message.args.split(' ')[1]
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('b', None)
				self.message_logger.channel('[{0}] {1} for {2}'.format(command_name, self.message.message, channel.name))
				self.broadcast_channel_info(channel)
			elif self.message.command == '372':
				# 372 RPL_MOTD (RFC 1459)
				command_name = '372 RPL_MOTD'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '375':
				# 375 RPL_MOTDSTART (RFC 1459)
				command_name = '375 RPL_MOTDSTART'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '376':
				# 376 RPL_ENDOFMOTD (RFC 1459)
				command_name = '376 RPL_ENDOFMOTD'
				self.message_logger.info('[{0}] {1}'.format(command_name, self.message.message))
			elif self.message.command == '381':
				# 381 RPL_YOUREOPER (RFC 1459)
				command_name = '381 RPL_YOUREOPER'
				self.identity.is_oper = True
				self.broadcast_server_info(self.network.network)
				self.message_logger.notice('[{0}] {1}'.format(command_name, self.message.message))
				self.notify_owners_of_error('warning', self.message.message)
			elif self.message.command == '385':
				# 385 RPL_NOTOPERANYMORE (RFC 1459)
				command_name = '385 RPL_NOTOPERANYMORE'
				self.identity.is_oper = False
				self.broadcast_server_info(self.network.network)
				self.message_logger.notice('[{0}] {1}'.format(command_name, self.message.message))
				self.notify_owners_of_error('warning', self.message.message)
			elif self.message.command == '386':
				# 386 RPL_QLIST (InspIRCd, Unreal, ???)
				# 386 RPL_IRCOPS (Ultimate, ???)
				# On some IRCds (e.g. InspIRCd), +q is for founders; on others (e.g. ircd-seven), it is for silenced ("quiet") users
				command_name = '386 RPL_QLIST'
				args_list = self.get_list_from_numeric_args(self.message.args)
				channel_name, q_nick = args_list
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('q', q_nick)
				self.message_logger.channel('[{0}] {1} is a founder on {2}'.format(command_name, q_nick, channel.name))
			elif self.message.command == '387':
				# 387 RPL_ENDOFQLIST (InspIRCd, Unreal, ???)
				# 387 RPL_ENDOFIRCOPS (Ultimate, ???)
				command_name = '387 RPL_ENDOFQLIST'
				channel_name = self.message.args.split(' ')[1]
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('q', None)
				self.message_logger.channel('[{0}] {1} for {2}'.format(command_name, self.message.message, channel.name))
			elif self.message.command == '388':
				# 388 RPL_ALIST (InspIRCd, Unreal, ???)
				command_name = '388 RPL_ALIST'
				channel_name, protected_nick = self.get_list_from_numeric_args(self.message.args)
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('a', protected_nick)
				self.message_logger.channel('[{0}] {1} is a protected nick on {2}'.format(protected_nick, channel.name))
			elif self.message.command == '389':
				# 389 RPL_ENDOFALIST (InspIRCd, Unreal, ???)
				command_name = '389 RPL_ENDOFALIST'
				channel_name = self.message.args[1]
				channel = self.channels.channels[channel_name]
				channel.rebuild_mode_list('a', None)
				self.message_logger.info('[{0}] {1} for {2}'.format(command_name, self.message.message, channel.name))
			elif self.message.command == '396':
				# 396 RPL_HOSTHIDDEN (ircd-seven, Undernet, InspIRCd, ???)
				command_name = '396 RPL_HOSTHIDDEN'
				self.message_logger.notice('[{0}] {1} {2}'.format(command_name, self.message.args.split(' ')[1], self.message.message))
			elif self.message.command == '401':
				# 401 ERR_NOSUCHNICK (RFC1459, RFC2812)
				command_name = '401 ERR_NOSUCHNICK'
				nick = self.message.args.split(' ')[1]
				log_message = '[{0}] ({1}) {2}'.format(command_name, self.message.message, nick)
				self.message_logger.warning(log_message)
				self.notify_owners_of_error('info', log_message)
			elif self.message.command == '404':
				# 404 ERR_CANNOTSENDTOCHAN (RFC1459, RFC2812)
				command_name = '404 ERR_CANNOTSENDTOCHAN'
				channel_name = self.message.args.split(' ')[1]
				log_message = 'Sending message to {0} failed: {1}'.format(channel_name, self.message.message)
				self.message_logger.warning('[{0}] {1}'.format(command_name, log_message))
				self.notify_owners_of_error('info', log_message)
			elif self.message.command == '421':
				# 421 ERR_UNKNOWNCOMMAND (RFC1459, RFC2812)
				command_name = '421 ERR_UNKNOWNCOMMAND'
				command = self.message.args.split(' ')[1]
				log_message = '[{0}] {1} {2}'.format(command_name, self.message.message, command)
				self.message_logger.warning(log_message)
				self.notify_owners_of_error('info', log_message)
			elif self.message.command == '433':
				# 433 ERR_NICKNAMEINUSE (RFC1459, RFC2812)
				command_name = '433 ERR_NICKNAMEINUSE'
				if self.message.message is not None:
					nick_in_use = self.message.message
				else:
					nick_in_use = self.message.args
				log_message = 'Nick {} is in use.'.format(nick_in_use)
				self.message_logger.warning('[{0}] {1}'.format(command_name, log_message))
				if self.identity.identified_at_startup:
					self.notify_owners_of_error('warning', log_message)
				else:
					self.identity.nick_attempt += 1
					try:
						self.identify(self.identity.nicks[self.identity.nick_attempt])
					except KeyError:
						self.die('All specified nicks are in use on {0} ({1)'.format(self.network.name, self.network.address))
					else:
						self.identity.nick = self.identity.nicks[self.identity.nick_attempt]
						log_message = 'Successfully identified with the nick {}'.format(self.identity.nick)
						self.message_logger.info('[{0}] {1}'.format(command_name, log_message))
						self.notify_owners_of_error('info', log_message)
						self.identity.identified_at_startup = True
			elif self.message.command == '443':
				# 443 ERR_USERONCHANNEL (RFC1459, RFC2812)
				command_name = '443 ERR_USERONCHANNEL'
				nick, channel_name = self.message.args.split(' ')
				self.message_logger.warning('[{0}] {1} {2} {3}'.format(command_name, nick, self.message.message, channel))
			elif self.message.command == '470':
				# 470 ERR_LINKCHANNEL (Unreal, ircd-seven, charybdis)
				# 470 ERR_KICKEDFROMCHAN (aircd)
				command_name = '470 ERR_LINKCHANNEL'
				attempted_channel_name, redir_channel_name = self.get_list_from_numeric_args(self.message.args)
				log_message = 'Could not join {0}, redirected to {1}.'.format(attempted_channel_name, redir_channel_name)
				self.message_logger.warning('[{0}] {1}'.format(command_name, log_message))
				self.notify_owners_of_error('info', log_message)
			elif self.message.command == '471':
				# 471 ERR_CHANNELISFULL (RFC1459, RFC2812)
				command_name = '471 ERR_CHANNELISFULL'
				log_message = 'Can\'t join {} (channel is full)'.format(self.message.args.split(' ')[1])
				self.message_logger.warning('[{0}] {1}'.format(command_name, log_message))
				self.notify_owners_of_error('info', log_message)
			elif self.message.command == '472':
				# 472 ERR_UNKNOWNMODE (RFC1459, RFC2812)
				command_name = '472 ERR_UNKNOWNMODE'
				mode_char = self.message.args
				self.message_logger.warning('[{0}] {1} {2}'.format(command_name, mode_char, self.message.message))
			elif self.message.command == '473':
				# 473 ERR_INVITEONLYCHAN (RFC1459, RFC2812)
				command_name = '473 ERR_INVITEONLYCHAN'
				channel_name = self.message.args.split(' ')[1]
				log_message = 'Can\'t join {} (invite only)'.format(channel)
				self.message_logger.warning('[{0}] {1}'.format(command_name, log_message))
				self.notify_owners_of_error('info', log_message)
				if self.channels.knock_if_not_invited:
					self.write('KNOCK {}'.format(channel_name))
			elif self.message.command == '474':
				# 473 ERR_BANNEDFROMCHAN (RFC1459, RFC2812)
				command_name = '473 ERR_BANNEDFROMCHAN'
				log_message = 'Can\'t join {} (banned)'.format(self.message.args.split(' ')[1])
				self.message_logger.error('[{0}] {1}'.format(command_name, log_message))
				self.notify_owners_of_error('warning', log_message)
			elif self.message.command == '475':
				# 473 ERR_BADCHANNELKEY (RFC1459, RFC2812)
				command_name = '473 ERR_BADCHANNELKEY'
				log_message = 'Can\'t join {} (incorrect/missing key)'.format(self.message.args.split(' ')[1])
				self.message_logger.error('[{0}] {1}'.format(command_name, log_message))
				self.notify_owners_of_error('warning', log_message)
			elif self.message.command == '481':
				# 481 ERR_NOPRIVILEGES (RFC1459, RFC2812)
				command_name = '481 ERR_NOPRIVILEGES'
				self.message_logger.warning('[{0}] {1}'.format(command_name, self.message.message))
				self.notify_owners_of_error('warning', self.message.message)
			elif self.message.command == '482':
				# 482 ERR_CHANOPRIVSNEEDED (RFC 1459)
				command_name = '482 ERR_CHANOPRIVSNEEDED'
				log_message = '{0} on {1}'.format(self.message.message, self.message.args.split(' ')[1])
				self.message_logger.warning('[{0}] {1}'.format(command_name, log_message))
				self.notify_owners_of_error('info', log_message)
			elif self.message.command == '900':
				# 900 RPL_LOGGEDIN "You are now logged in as {nick} to {server}" (SASL)
				# http://ircv3.atheme.org/extensions/sasl-3.1
				command_name = '900 RPL_LOGGEDIN'
				self.message_logger.info('[{0}] {1} to {2}'.format(command_name, self.message.message, self.message.origin))
			elif self.message.command == 'PING':
				if self.network.detect_timeout_value:
					if self.network.detected_timeout is None:
						if self.network.network.time_of_last_ping is not None:
							self.network.detected_timeout = time.time() - self.network.network.time_of_last_ping
							event_logger.debug('Detected timeout length: {}'.format(self.network.detected_timeout))
							self.network.ping_timeout = self.network.detected_timeout + 10
						self.network.network.time_of_last_ping = time.time()
					else:
						self.network.network.time_of_last_ping = time.time()
				else:
					self.network.network.time_of_last_ping = time.time()
				self.message_logger.debug('[{0}] Time of last ping: {1}'.format(self.message.command, self.network.network.time_of_last_ping))
				self.pong()
			elif self.message.command == 'PONG':
				self.message_logger.notice('[{0}] Got pong from {1}'.format(self.message.command, self.message.args))
				self.network.network.got_pong = True
			elif self.message.command == 'JOIN':
				if self.message.message is not None:
					channel_name = self.message.message
				else:
					channel_name = self.message.args
				if self.message.nick == self.identity.nick:
					self.notify_owners_of_error('info', '{0} has joined {1}'.format(self.message.nick, channel_name))
					self.message_logger.channel('[{0}] {1} has joined {2}'.format(self.message.command, self.message.nick, channel_name))
					chan_names = set()
					for chan_name in self.channels.channels.keys():
						chan_names.add(chan_name)
					if channel_name in chan_names:
						event_logger.warning('Joined channel {}, but it was already in the channel list.'.format(channel_name))
					else:
						self.channels.channels.update({channel_name:Channel(self.network.network, channel_name)})
						self.mode(channel_name)
						self.update_channel_info(channel_name)
				else:
					self.message_logger.channel('[{0}] {1} ({2}) has joined {3}'.format(self.message.command, self.message.nick, self.message.mask, channel_name))
					self.get_names(channel_name)
			elif self.message.command == 'NICK':
				if self.message.message is None:
					old_nick, new_nick = self.message.nick, self.message.args
				else:
					old_nick, new_nick = self.message.nick, self.message.message
				self.message_logger.channel('[{0}] {1} is now known as {2}'.format(self.message.command, old_nick, new_nick))
				for channel in self.channels.channels.values():
					if self.message.nick in channel.nicks:
						self.get_names(channel.name)
						self.broadcast_channel_info(channel)
				if self.identity.nick == old_nick:
					self.identity.nick = new_nick
					self.broadcast_server_info(self.network.network)
			elif self.message.command == 'MODE':
				args_list = self.message.args.split(' ')
				target = args_list.pop(0)
				if self.message.message is None:
					mode = ' '.join(args_list)
				else:
					mode = self.message.message
				_modes = mode.split(' ')[0][1:]
				self.message_logger.debug(_modes)
				if self.message.nick:
					op = self.message.nick
				else:
					op = self.message.origin
				if target[:1] in self.network.network.chantypes:
					self.dest_is_channel = True
					did_names = False
					did_mode = False
					for _mode in _modes:
						if _mode in self.network.network.chanmodetypes['list']:
							self.mode(target, _mode)
						elif _mode in self.network.network.supported['prefix'].values():
							if not did_names:
								self.get_names(target)
								did_names = True
						else:
							if not did_mode:
								self.mode(target)
								did_mode = True
					self.message_logger.channel('[{0}] {1} sets mode: {2} {3}'.format(self.message.command, op, target, mode))
				else:
					self.dest_is_channel = False
					self.message_logger.notice('[{0}] {1} sets mode: {2} {3}'.format(self.message.command, op, target, mode))
					if target == self.identity.nick:
						self.write('MODE {}'.format(self.identity.nick))
			elif self.message.command == 'KICK':
				channel_name, nick = self.message.args.split(' ')
				if self.message.nick is not None:
					kicker = self.message.nick
				else:
					kicker = self.message.origin
				log_message = '{0} was kicked from {1} by {2} ({3})'.format(nick, channel_name, kicker, self.message.message)
				if nick == self.identity.nick:
					self.message_logger.warning('[{0}] {1}'.format(self.message.command, log_message))
					self.notify_owners_of_error('info', log_message)
					self.remove_channel_from_list(channel_name)
					if self.channels.rejoin_if_kicked:
						event_logger.info('Trying to rejoin {}...'.format(channel_name))
						self.join(channel_name)
				else:
					self.message_logger.channel('[{0}] {1}'.format(self.message.command, log_message))
					self.get_names(channel_name)
			elif self.message.command == 'INVITE':
				channel_name = self.message.message
				if self.message.nick is not None:
					inviter = self.message.nick
					cloak = self.message.cloak
				else:
					inviter = self.message.origin
					cloak = self.message.origin
				if self.channels.accept_invites:
					if self.channels.join_when_invited_by_default:
						if channel_name in self.channels.invite_blacklist:
							self.message_logger.info('[{0}] Ignored invite to {1} from {2} ({3})'.format(self.message.command, channel_name, inviter, cloak))
						else:
							log_message = 'Accepted invite to {0} from {1} ({2})'.format(channel_name, inviter, cloak)
							self.message_logger.info('[{0}] {1}'.format(self.message.command, log_message))
							self.notify_owners_of_error('info', log_message)
							self.join(channel_name)
					else:
						if channel_name in self.channels.invite_whitelist:
							log_message = 'Accepted invite to {0} from {1} ({2})'.format(channel_name, inviter, cloak)
							self.message_logger.warning(log_message)
							self.notify_owners_of_error('info', log_message)
							self.join(channel_name)
						else:
							self.message_logger.info('[{0}] Ignored invite to {1} from {2} ({3})'.format(self.message.command, channel_name, inviter, cloak))
				else:
					self.message_logger.info('[{0}] Ignored invite to {1} from {2} ({3})'.format(self.message.command, channel_name, inviter, cloak))
			elif self.message.command == 'TOPIC':
				log_message = 'Topic for {args} was changed to "{message}" by {nick} ({cloak})'.format(**self.message.__dict__)
				self.message_logger.channel('[{0}] {1}'.format(self.message.command, log_message))
				self.notify_owners_of_error('info', log_message)
				channel_name = self.message.args
				channel = self.channels.channels[channel_name]
				channel.topic = self.message.message
				self.broadcast_channel_info(channel)
			elif self.message.command == 'PRIVMSG':
				if self.message.args[:1] in self.network.network.chantypes:
					self.message.source = self.message.args 
				else:
					if self.message.nick is not None:
						self.message.source = self.message.nick
					else:
						self.message.source = self.message.origin
				self.message_logger.chat('[{0}] {1}'.format(self.message.command, self.message_logs.privmsg_formatting.format(**self.message.__dict__)))
				if send_message_to_modules:
					self.message_dispatch_queue.put(self.message)
			elif self.message.command == 'NOTICE':
				try:
					chantypes = self.network.network.chantypes
				except AttributeError:
					self.message.source = self.message.origin
				else:
					if self.message.args[:1] in chantypes:
						self.message.source = self.message.args 
					else:
						if self.message.nick is not None:
							self.message.source = self.message.nick
						else:
							self.message.source = self.message.origin
				self.message_logger.notice('[{0}] {1}'.format(self.message.command, self.message_logs.notice_formatting.format(**self.message.__dict__)))
				if self.message.message == '*** Looking up your hostname...' and self.message.mask is None:
					self.identify(self.identity.nicks[self.identity.nick_attempt])
				if self.message.nick is not None:
					lowercase_nick = self.message.nick.lower()
				else:
					lowercase_nick = None
				if lowercase_nick == self.network.nick_service.lower():
					message_list = self.message.message.split(' ')
					if message_list[0].lower() == 'status':
						self.message_logger.debug('Got STATUS message from services.')
						reg_nick, status_code = message_list[1], message_list[2]
						self.message_logger.debug('Jobs: {}'.format(len(self.pending_check_registration_jobs)))
						for job in self.pending_check_registration_jobs:
							if reg_nick == job.nick:
								job.set_status_code(status_code)
								job.is_pending = False
								self.message_logger.debug('Processed reg check job for {}.'.format(job.nick))
								break
						if not job.is_pending:
							self.pending_check_registration_jobs.remove(job)
							self.finished_check_registration_jobs.put(job)
			elif self.message.command == 'PART':
				if self.message.message is not None:
					self.message_logger.channel('[{command}] {nick} ({mask}) has left {args} ({message})'.format(**self.message.__dict__))
				else:
					self.message_logger.channel('[{command}] {nick} ({mask}) has left {args}'.format(**self.message.__dict__))
				channel_name = self.message.args
				if self.message.nick == self.identity.nick:
					self.remove_channel_from_list(channel_name)
					if self.message.message:
						if 'Removed' in self.message.message:
							self.notify_owners_of_error('warning', '{0} seems to have been removed from {1}'.format(self.message.nick, channel_name))
				else:
					self.get_names(channel_name)
			elif self.message.command == 'QUIT':
				if self.message.message is not None:
					self.message_logger.channel('[{command}] {nick} ({mask}) Quit ({message})'.format(**self.message.__dict__))
				else:
					self.message_logger.channel('[{command}] {nick} ({mask}) Quit'.format(**self.message.__dict__))
				for channel_name in self.channels.channels.keys():
					if self.message.nick in self.channels.channels[channel_name].nicks:
						self.get_names(channel_name)
			elif self.message.command == 'ERROR':
				self.message_logger.fatal('[{command}] Disconnected ({message})'.format(**self.message.__dict__))
				self.die('Disconnected.', notify_owners=False)
			else:
				message_is_valid = False
				if self.message.command is not None:
					self.message_logger.error('Unrecognized message type. Please submit an issue report at https://code.google.com/p/noisebot/issues/list')
					self.message_logger.error(self.message.raw)
				else:
					self.message_logger.error('IRC command was None! Please submit an issue report at https://code.google.com/p/noisebot/issues/list')
					self.message_logger.error(self.message.raw)
					self.notify_owners_of_error('error', 'IRC command was None! Please submit an issue at https://code.google.com/p/noisebot/issues/list')
			if message_is_valid and send_message_to_modules:
				self.message_broadcast_queue.put(self.message)

	@die_if_error(name='Command Broadcast')
	def broadcast_command(self):
		"""
		Broadcast command messages to interested modules.
		"""
		while self.alive:
			irc_message = self.message_dispatch_queue.get()
			message = irc_message.message
			message_list = message.split(' ')
			first_word = message_list.pop(0)
			if first_word[:1] == self.identity.trigger and first_word[1:] in self.commands:
				command = first_word[1:]
				event_logger.debug(command)
				args = ' '.join(message_list)
				if command in self.admin_commands:
					if self.user_is_owner(irc_message.nick, irc_message.cloak):
						self.process_admin_command(irc_message.nick, irc_message.source, command, args)
					else:
						event_logger.debug('Using nick auth: {}'.format(self.users.use_nick_auth))
						event_logger.debug('Invoker\'s nick: {}'.format(irc_message.nick))
						event_logger.debug('Allowed nicks: {}'.format(' '.join(self.users.owner_nicks)))
						self.notice('Only an owner may use this command.', irc_message.nick)
				elif command in self.public_commands:
					self.process_public_command(irc_message, command)
				else:
					event_logger.debug('Module command caught.')
					modules = frozenset(self.modules.modules.values())
					for module in modules:
						if (command in module.commands) and module.recv_commands:
							self.send_irc_message_to_module(module, command, irc_message)
			self.message_dispatch_queue.task_done()

	def send_message_to_module_wrapper(self, message_dict, which_module):
		message_json = json_encoder.encode(message_dict)
		if which_module is None:
			modules = frozenset(self.modules.modules.values())
			for module in modules:
				self.send_json_to_module(module, message_json)
		else:
			self.send_json_to_module(which_module, message_json)

	@die_if_error(name='Event Broadcast')
	def broadcast_event(self):
		"""
		Broadcast event messages to interested modules.
		"""
		while self.alive:
			message = self.message_broadcast_queue.get()
			modules = frozenset(self.modules.modules.values())
			for module in modules:
				if module is not None:
					if module.recv_events and message.command in module.recv_message_types:
						self.send_irc_message_to_module(module, None, message)
			self.message_broadcast_queue.task_done()

	def broadcast_server_info(self, server, which_module=None):
		try:
			message_dict = {'action':'server_info',
						'nick':self.identity.nick,
						'umodes':self.identity.umodes,
						'is_oper':self.identity.is_oper,
						'trigger':self.identity.trigger,
						'config_network_name':self.network.network.config_network_name,
						'network_name':self.network.network.name,
						'rpl_isupport':self.network.network.supported}
		except AttributeError: # not all info is available
			return
		self.send_message_to_module_wrapper(message_dict, which_module)
	
	def broadcast_server_dc_notice(self, server, which_module=None):
		message_dict = {'action':'server_info',
					'is_on_network':False,
					'network_name_config':self.network.network.config_network_name,
					'network_name':self.network.network.supported['network'],
					'rpl_isupport':None}
		self.send_message_to_module_wrapper(message_dict, which_module)

	def broadcast_channel_leave_notice(self, channel, which_module=None):
		message_dict = {'action':'channel_info', 'network_name':channel.server.name,
			'channel_name':channel.name, 'is_in_channel':False,
			'nicks':None, 'modes':None,	'topic':None, 'creation_time':None}
		self.send_message_to_module_wrapper(message_dict, which_module)

	def broadcast_channel_info(self, channel, which_module=None):
		message_dict = {'action':'channel_info', 'network_name':channel.server.name,
			'channel_name':channel.name, 'is_in_channel':True,
			'nicks':channel.nicks, 'modes': channel.modes,
			'topic':channel.topic, 'creation_time':channel.creation_time}
		self.send_message_to_module_wrapper(message_dict, which_module)

	def broadcast_module_info(self):
		message_dict = {'action':'module_info'}
		modules = self.modules.modules.copy()
		message_dict.update(loaded_modules=modules.keys(), base_commands=self.core_commands, all_commands=self.commands)
		for module in modules.values():
			message_dict['module_name'] = module.name
			self.send_message_to_module_wrapper(message_dict, module)

	def send_irc_message_to_module(self, module, command, irc_message):
		message_dict = copy.deepcopy(irc_message.__dict__)
		message_dict['sender_is_owner'] = self.user_is_owner(irc_message.nick, irc_message.cloak)
		message_dict['config_network_name'] = self.network.network.config_network_name
		try:
			channel = self.channels.channels[irc_message.source]
		except KeyError:
			message_dict['sender_ops'] = None
		else:
			message_dict['sender_ops'] = self.user_ops(irc_message.nick, channel)
		if message_dict['message'] is not None:
			msg = message_dict['message'].split(' ')
			del msg[0]
			msg = ' '.join(msg)
			message_dict['message'] = msg
		message_dict['command'] = command
		message_dict['action'] = 'irc_message'
		del message_dict['raw_bytes']
		if command is not None:
			del message_dict['raw']
			del message_dict['args']
			del message_dict['origin']
		message_json = json_encoder.encode(message_dict)
		self.send_json_to_module(module, message_json)

	def send_ping_message_to_module(self, module):
		self.send_json_to_module(module, json_encoder.encode({'action':'ping'}))

	def send_pong_message_to_module(self, module):
		self.send_json_to_module(module, json_encoder.encode({'action':'pong'}))
	
	def send_die_message_to_module(self, module, reason):
		message_dict = {'action': 'die', 'reason': reason}
		self.send_json_to_module(module, json_encoder.encode(message_dict))
	
	def send_error_message_to_module(self, module, error_message):
		message_json = json_encoder.encode('{{"action":"error", "message":"{}"}}'.format(error_message))
		self.send_json_to_module(module, message_json)
	
	def send_json_to_module(self, module, message_json):
		if module is not None:
			if module in self.modules.modules.values():
				try:
					module.connection.send(message_json)
				except (IOError, OSError) as exc:
					if self.alive:
						event_logger.exception('Connection to {} module seems to be broken:'.format(module.name))
						self.notify_owners_of_error('warning', 'The {} module died.'.format(module.name))
					self.unregister_module(module)
					if self.modules.exit_if_module_fails_at_runtime:
						self.die('The {} module died.'.format(module.name), exc)
				except Exception as exc:
					event_logger.exception('Unexpected error encountered when trying to end message to {} module.'.format(module.name))
					self.unregister_module(module)
					if self.modules.exit_if_module_fails_at_runtime:
						self.die('The {} module died.'.format(module.name), exc)
				else:
					event_logger.debug('Sent message to {0}: {1}'.format(module.name, message_json))
					module.is_responsive = True
			else:
				event_logger.warning('A message could not be sent to the {} module because it is not registered.'.format(module.name))
		else:
			event_logger.error('Tried to send a message to a module that isn\'t registered/loaded. This is a bug.')
		event_logger.debug('Receiving module messages: {}'.format(self.recv_module_messages))
		event_logger.debug('Module message receive thread is alive: {}'.format(self.recv_module_messages_thread.is_alive()))

	@die_if_error(name='Module Message Receive')
	def recv_messages_from_modules(self):
		event_logger.debug('Module message receive loop started.')
		while self.alive:
			time.sleep(0.15)
			modules = frozenset(self.modules.modules.values())
			for module in modules:
				if module is None:
					#event_logger.debug('Tried to process a module being registered?')
					continue
				#event_logger.debug('Checking {} module for incoming messages.'.format(module.name))
				if module.connection_thread.is_alive():
					try:
						incoming_json = module.incoming_json.popleft()
					except IndexError:
						continue	
					event_logger.debug('Message from {0}: {1}'.format(module.name, incoming_json))
					module.is_available = True
					try:
						message_dict = json_decoder.decode(incoming_json)
					except ValueError:
						self.send_error_message_to_module(module, 'Last message was malformed: {}'.format(sys.exc_info()[1]))
						continue
					event_logger.debug('Incoming message: {}'.format(message_dict))
					try:
						action = message_dict['action']
					except KeyError:
						self.send_error_message_to_module(module, 'Last message was missing the required "action" field: {}'.format(incoming_json))
						continue
					if action == 'ping':
						self.send_pong_message_to_module(module)
					elif action == 'pong':
						event_logger.info('Received pong from {} module'.format(module.name))
					elif action == 'error':
						try:
							message = message_dict['message']
						except KeyError:
							self.send_error_message_to_module(module, 'Last message was missing the required "message" field: {}'.format(incoming_json))
							event_logger.warning('The {0} module passed an error message, but didn\'t give details.'.format(module.name))
						else:
							event_logger.warning('The {0} module passed an error message: {1}'.format(module.name, message))
					elif action == 'irc_message':
						try:
							self.relay_irc_message_to_server(message_dict)
						except ValueError:
							event_logger.warning(self.incoming_irc_message_error)
							self.incoming_irc_message_error = None
							self.send_error_message_to_module(module, 'Last IRC message was invalid: {}'.format(incoming_json))
					elif action == 'die':
						log_message = 'The {0} module terminated gracefully with this message: {1}'.format(module.name, message_dict['message'])
						event_logger.warning(log_message)
						self.unregister_module(module)
						self.notify_owners_of_error('warning', log_message)
					else:
						self.send_error_message_to_module(module, 'Last message was missing a valid value for the "action" field: {}'.format(incoming_json))
				else:
					event_logger.error('The {} module\'s connection thread died. The module will be unloaded'.format(module.name))
					self.unregister_module(module)
		event_logger.debug('Module message receive loop terminated.')
	
	def relay_irc_message_to_server(self, message_dict):
		try:
			args = message_dict['args']
		except KeyError:
			self.incoming_irc_message_error = 'Tried to relay a message to the server that lacks the args field from the {{}} module: {}'.format(message_dict)
			raise ValueError
		try:
			message = message_dict['message']
		except KeyError:
			self.incoming_irc_message_error = 'Tried to relay a message to the server that lacks the message field from the {{}} module: {}'.format(message_dict)
			raise ValueError
		#else:
		#	message_bytes = message.encode('utf-8')
		#	message = message_bytes.decode('raw_unicode_escape')
		try:
			message_type = message_dict['message_type']
		except KeyError:
			self.incoming_irc_message_error = 'Tried to relay a message to the server that lacks the message_type field from the {{}} module: {}'.format(message_dict)
			raise ValueError
		if message_type.lower() == 'raw':
			self.write(message)
		elif message_type.lower() in ('user', 'service', 'privmsg', 'notice', 'squery', 'kill'):
			# args and message are both required
			if message is not None and args:
				if message_type.lower() in ('privmsg', 'notice', 'kill'):
					self.message_logger.info('{0} sent to {1}: {2}'.format(message_type.upper(), args, message))
				else:
					self.message_logger.debug('{0} sent to {1}: {2}'.format(message_type.upper(), args, message))
				self.write('{0} {1} :{2}'.format(message_type, args, message))
			else:
				self.incoming_irc_message_error = 'A module sent an invalid {0} message: {1}'.format(message_type, message_dict)
				raise ValueError
		elif message_type.lower() in ('part', 'topic', 'squit', 'kick', 'knock', 'remove', 'fpart'):
			# args mandatory, message optional
			if args:
				if message is not None:
					self.write('{0} {1} :{2}').format(message_type, args, message)
				else:
					self.write('{0} {1}').format(message_type, args)
			else:
				self.incoming_irc_message_error = 'A module sent an invalid {0} message (no args for a command that requires one): {1}'.format(message_type, message_dict)
				raise ValueError
		elif message_type.lower() in ('pass', 'nick', 'oper', 'mode', 'join', 'connect',
								'whois', 'whowas', 'ping', 'pong', 'summon', 'userhost', 'ison'):
			# args mandatory, no message
			if args:
				self.write('{0} {1}'.format(message_type, args))
			else:
				self.incoming_irc_message_error = 'The {{}} module sent an invalid {0} message: {1}'.format(message_type, message_dict)
				raise ValueError
		elif message_type.lower() in ('error', 'wallops'):
			# no args, message mandatory
			if message is not None:
				self.write('{0} :{1}'.format(message_type, message))
			else:
				self.incoming_irc_message_error = 'The {{}} module sent an invalid {0} message: {1}'.format(message_type, message_dict)
				raise ValueError
		elif message_type.lower() in ('quit', 'away'):
			# no args, message optional
			if message is not None:
				self.write('{0} :{1}'.format(message_type, message))
			else:
				self.write('{0}'.format(message_type))
		elif message_type.lower() in ('names', 'list', 'motd', 'lusers', 'version', 'stats',
								'links', 'time', 'trace', 'admin', 'info', 'who', 'users'):
			# args optional, no message
			if args:
				self.write('{0} {1}'.format(message_type, args))
			else:
				self.write('{0}'.format(message_type))
		elif message_type.lower() in ('rehash', 'die', 'restart'):
			# no args, no message
			self.write('{0}'.format(message_type))

	def notify_owners_of_error(self, error_level, message):
		message_level = self.owner_notification_levels[error_level]
		recv_level = self.owner_notification_levels[self.users.owner_notification_level]
		event_logger.debug('Message level: {0} ({1}); Receive level: {2} ({3})'.format(error_level, message_level, self.users.owner_notification_level, recv_level))
		if message_level <= recv_level:
			if event_log_rotation.use_utc:
				cur_time = time.gmtime(time.time())
			else:
				cur_time = time.localtime(time.time())
			timestamp = time.strftime('%Y-%m-%d %H:%M:%S', cur_time)
			message = '[{0}] {1}: {2}'.format(timestamp, error_level.upper(), message)
			event_logger.debug('Sending an error message!')
			if self.users.use_memo_service:
				for owner_nick in self.users.owners_who_recv_notifs:
					self.msg(self.network.memo_service, 'send {0} {1}'.format(owner_nick, message))
			else:
				try:
					for owner_nick in self.users.owners_who_recv_notifs:
						self.msg(owner_nick, message)
				except:
					event_logger.exception('Couldn\'t notify owners:')
					pass

if __name__ == '__main__':
	noisebot = NoiseBot()
